@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.
- package/.turbo/turbo-build.log +15 -13
- package/.turbo/turbo-dev.log +32 -0
- package/.turbo/turbo-test.log +54 -10
- package/dist/adapters/adapterApi.d.ts +1 -1
- package/dist/adapters/createTreeNode.js +32 -4
- package/dist/adapters/jsx/getExpressionData.d.ts +1 -1
- package/dist/adapters/jsx/getExpressionData.js +8 -4
- package/dist/adapters/jsx/getJSXComponentBoundingBox.d.ts +1 -1
- package/dist/adapters/jsx/getJSXComponentBoundingBox.js +11 -6
- package/dist/adapters/jsx/jsxAdapter.js +13 -8
- package/dist/adapters/nextjs/parseNextjsDataAttributes.d.ts +31 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +106 -0
- package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.d.ts +4 -0
- package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.js +218 -0
- package/dist/adapters/phoenix/detectPhoenix.d.ts +11 -0
- package/dist/adapters/phoenix/detectPhoenix.js +38 -0
- package/dist/adapters/phoenix/index.d.ts +10 -0
- package/dist/adapters/phoenix/index.js +9 -0
- package/dist/adapters/phoenix/parsePhoenixComments.d.ts +35 -0
- package/dist/adapters/phoenix/parsePhoenixComments.js +131 -0
- package/dist/adapters/phoenix/types.d.ts +16 -0
- package/dist/adapters/phoenix/types.js +1 -0
- package/dist/adapters/react/getFiberLabel.js +2 -1
- package/dist/components/MaybeOutline.js +65 -3
- package/dist/components/Runtime.js +1 -1
- package/dist/functions/formatAncestryChain.d.ts +3 -0
- package/dist/functions/formatAncestryChain.js +104 -15
- package/dist/functions/formatAncestryChain.test.js +26 -20
- package/dist/functions/normalizeFilePath.d.ts +14 -0
- package/dist/functions/normalizeFilePath.js +40 -0
- package/dist/output.css +87 -15
- package/dist/types/ServerComponentInfo.d.ts +14 -0
- package/dist/types/ServerComponentInfo.js +1 -0
- package/package.json +4 -3
- package/src/adapters/adapterApi.ts +1 -1
- package/src/adapters/createTreeNode.ts +35 -3
- package/src/adapters/jsx/getExpressionData.ts +9 -5
- package/src/adapters/jsx/getJSXComponentBoundingBox.ts +13 -8
- package/src/adapters/jsx/jsxAdapter.ts +14 -12
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +112 -0
- package/src/adapters/phoenix/__tests__/parsePhoenixComments.test.ts +264 -0
- package/src/adapters/phoenix/detectPhoenix.ts +44 -0
- package/src/adapters/phoenix/index.ts +11 -0
- package/src/adapters/phoenix/parsePhoenixComments.ts +140 -0
- package/src/adapters/phoenix/types.ts +16 -0
- package/src/adapters/react/getFiberLabel.ts +2 -1
- package/src/components/MaybeOutline.tsx +63 -4
- package/src/components/Runtime.tsx +3 -1
- package/src/functions/formatAncestryChain.test.ts +26 -20
- package/src/functions/formatAncestryChain.ts +121 -15
- package/src/functions/normalizeFilePath.ts +41 -0
- package/src/types/ServerComponentInfo.ts +14 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parsePhoenixServerComponents } from "../adapters/phoenix/parsePhoenixComments";
|
|
2
|
+
import { parseNextjsServerComponents } from "../adapters/nextjs/parseNextjsDataAttributes";
|
|
3
|
+
import { normalizeFilePath } from "./normalizeFilePath";
|
|
1
4
|
// Elements to exclude from ancestry (not useful for debugging)
|
|
2
5
|
const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
|
|
3
6
|
function isTreeNodeElement(node) {
|
|
@@ -22,7 +25,7 @@ function getNthChildIfAmbiguous(element) {
|
|
|
22
25
|
function treeNodeComponentToOwnerInfo(comp) {
|
|
23
26
|
return {
|
|
24
27
|
name: comp.label,
|
|
25
|
-
filePath: comp.callLink?.fileName,
|
|
28
|
+
filePath: comp.callLink?.fileName ? normalizeFilePath(comp.callLink.fileName) : undefined,
|
|
26
29
|
line: comp.callLink?.lineNumber
|
|
27
30
|
};
|
|
28
31
|
}
|
|
@@ -51,6 +54,19 @@ export function collectAncestry(node) {
|
|
|
51
54
|
if (nthChild !== undefined) {
|
|
52
55
|
item.nthChild = nthChild;
|
|
53
56
|
}
|
|
57
|
+
|
|
58
|
+
// Parse server components from various sources
|
|
59
|
+
// 1. Phoenix LiveView (HTML comments)
|
|
60
|
+
const phoenixComponents = parsePhoenixServerComponents(element);
|
|
61
|
+
|
|
62
|
+
// 2. Next.js Server Components (data-locatorjs attributes)
|
|
63
|
+
const nextjsComponents = parseNextjsServerComponents(element);
|
|
64
|
+
|
|
65
|
+
// Combine all server components
|
|
66
|
+
const allServerComponents = [...(phoenixComponents || []), ...(nextjsComponents || [])];
|
|
67
|
+
if (allServerComponents.length > 0) {
|
|
68
|
+
item.serverComponents = allServerComponents;
|
|
69
|
+
}
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
|
|
@@ -62,7 +78,7 @@ export function collectAncestry(node) {
|
|
|
62
78
|
// Use outermost component as the primary component name
|
|
63
79
|
item.componentName = outermost.label;
|
|
64
80
|
if (outermost.callLink) {
|
|
65
|
-
item.filePath = outermost.callLink.fileName;
|
|
81
|
+
item.filePath = normalizeFilePath(outermost.callLink.fileName);
|
|
66
82
|
item.line = outermost.callLink.lineNumber;
|
|
67
83
|
}
|
|
68
84
|
} else {
|
|
@@ -71,18 +87,18 @@ export function collectAncestry(node) {
|
|
|
71
87
|
if (component) {
|
|
72
88
|
item.componentName = component.label;
|
|
73
89
|
if (component.callLink) {
|
|
74
|
-
item.filePath = component.callLink.fileName;
|
|
90
|
+
item.filePath = normalizeFilePath(component.callLink.fileName);
|
|
75
91
|
item.line = component.callLink.lineNumber;
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
if (!item.filePath && source) {
|
|
80
|
-
item.filePath = source.fileName;
|
|
96
|
+
item.filePath = normalizeFilePath(source.fileName);
|
|
81
97
|
item.line = source.lineNumber;
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
// Only include items that have useful info (component name or
|
|
85
|
-
if (item.componentName || item.filePath) {
|
|
100
|
+
// Only include items that have useful info (component name, file path, or server components)
|
|
101
|
+
if (item.componentName || item.filePath || item.serverComponents) {
|
|
86
102
|
items.push(item);
|
|
87
103
|
}
|
|
88
104
|
current = current.getParent();
|
|
@@ -101,8 +117,35 @@ export function formatAncestryChain(items) {
|
|
|
101
117
|
const indent = " ".repeat(index);
|
|
102
118
|
const prefix = index === 0 ? "" : "└─ ";
|
|
103
119
|
|
|
104
|
-
//
|
|
105
|
-
|
|
120
|
+
// Get the previous item's component to detect component boundaries
|
|
121
|
+
const prevItem = index > 0 ? reversed[index - 1] : null;
|
|
122
|
+
const prevComponentName = prevItem?.componentName || prevItem?.ownerComponents?.[prevItem.ownerComponents.length - 1]?.name;
|
|
123
|
+
|
|
124
|
+
// Get current item's innermost component
|
|
125
|
+
const currentComponentName = item.ownerComponents?.[item.ownerComponents.length - 1]?.name || item.componentName;
|
|
126
|
+
|
|
127
|
+
// Determine the display name for the element
|
|
128
|
+
// Use component name ONLY when crossing a component boundary (root element of a component)
|
|
129
|
+
// This prevents "App -> App:nth-child(5)" when both are just elements inside App
|
|
130
|
+
let displayName = item.elementName;
|
|
131
|
+
let outerComponents = [];
|
|
132
|
+
const isComponentBoundary = currentComponentName && currentComponentName !== prevComponentName;
|
|
133
|
+
if (isComponentBoundary) {
|
|
134
|
+
if (item.ownerComponents && item.ownerComponents.length > 0) {
|
|
135
|
+
// Use innermost component as display name, show outer ones in "in X > Y"
|
|
136
|
+
const innermost = item.ownerComponents[item.ownerComponents.length - 1];
|
|
137
|
+
if (innermost) {
|
|
138
|
+
displayName = innermost.name;
|
|
139
|
+
// Outer components (excluding innermost)
|
|
140
|
+
outerComponents = item.ownerComponents.slice(0, -1).map(c => c.name);
|
|
141
|
+
}
|
|
142
|
+
} else if (item.componentName) {
|
|
143
|
+
displayName = item.componentName;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build element selector: displayName:nth-child(n)#id
|
|
148
|
+
let selector = displayName;
|
|
106
149
|
if (item.nthChild !== undefined) {
|
|
107
150
|
selector += `:nth-child(${item.nthChild})`;
|
|
108
151
|
}
|
|
@@ -111,14 +154,60 @@ export function formatAncestryChain(items) {
|
|
|
111
154
|
}
|
|
112
155
|
let description = selector;
|
|
113
156
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
157
|
+
// Build component description parts
|
|
158
|
+
const parts = [];
|
|
159
|
+
|
|
160
|
+
// Server components (Phoenix/Next.js/Rails/etc.)
|
|
161
|
+
if (item.serverComponents && item.serverComponents.length > 0) {
|
|
162
|
+
// Group server components by framework (detected by file extension)
|
|
163
|
+
const phoenixComponents = item.serverComponents.filter(sc => sc.filePath.match(/\.(ex|exs|heex)$/));
|
|
164
|
+
const nextjsComponents = item.serverComponents.filter(sc => sc.filePath.match(/\.(tsx?|jsx?)$/));
|
|
165
|
+
|
|
166
|
+
// Format Phoenix components
|
|
167
|
+
if (phoenixComponents.length > 0) {
|
|
168
|
+
const names = phoenixComponents.filter(sc => sc.type === "component").map(sc => sc.name);
|
|
169
|
+
if (names.length > 0) {
|
|
170
|
+
parts.push(`[Phoenix: ${names.join(" > ")}]`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Format Next.js components
|
|
175
|
+
if (nextjsComponents.length > 0) {
|
|
176
|
+
const names = nextjsComponents.filter(sc => sc.type === "component").map(sc => sc.name);
|
|
177
|
+
if (names.length > 0) {
|
|
178
|
+
parts.push(`[Next.js: ${names.join(" > ")}]`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Client components - show outer components for context (if any)
|
|
184
|
+
if (outerComponents.length > 0) {
|
|
185
|
+
parts.push(`in ${outerComponents.join(" > ")}`);
|
|
186
|
+
}
|
|
187
|
+
if (parts.length > 0) {
|
|
188
|
+
description = `${selector} ${parts.join(" ")}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build location string
|
|
192
|
+
const locationParts = [];
|
|
193
|
+
|
|
194
|
+
// Server component locations
|
|
195
|
+
if (item.serverComponents && item.serverComponents.length > 0) {
|
|
196
|
+
item.serverComponents.forEach(sc => {
|
|
197
|
+
const prefix = sc.type === "caller" ? " (called from)" : "";
|
|
198
|
+
locationParts.push(`${sc.filePath}:${sc.line}${prefix}`);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Client component location (only add if different from server components)
|
|
203
|
+
if (item.filePath) {
|
|
204
|
+
const clientLocation = `${item.filePath}:${item.line}`;
|
|
205
|
+
// Only add if this location isn't already in the list
|
|
206
|
+
if (!locationParts.includes(clientLocation)) {
|
|
207
|
+
locationParts.push(clientLocation);
|
|
208
|
+
}
|
|
120
209
|
}
|
|
121
|
-
const location =
|
|
210
|
+
const location = locationParts.length > 0 ? ` at ${locationParts.join(", ")}` : "";
|
|
122
211
|
lines.push(`${indent}${prefix}${description}${location}`);
|
|
123
212
|
});
|
|
124
213
|
return lines.join("\n");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { formatAncestryChain } from "./formatAncestryChain";
|
|
3
3
|
describe("formatAncestryChain", () => {
|
|
4
|
-
it("
|
|
4
|
+
it("uses component name only at component boundaries", () => {
|
|
5
5
|
const items = [{
|
|
6
6
|
elementName: "button",
|
|
7
7
|
componentName: "Button"
|
|
@@ -10,23 +10,25 @@ describe("formatAncestryChain", () => {
|
|
|
10
10
|
componentName: "App"
|
|
11
11
|
}];
|
|
12
12
|
const result = formatAncestryChain(items);
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// App is root (component boundary), Button is different component (boundary)
|
|
14
|
+
expect(result).toBe(`App
|
|
15
|
+
└─ Button`);
|
|
15
16
|
});
|
|
16
|
-
it("
|
|
17
|
+
it("shows element name when same component as parent", () => {
|
|
17
18
|
const items = [{
|
|
18
19
|
elementName: "button",
|
|
19
|
-
componentName: "
|
|
20
|
+
componentName: "App",
|
|
20
21
|
id: "submit-btn"
|
|
21
22
|
}, {
|
|
22
23
|
elementName: "div",
|
|
23
24
|
componentName: "App"
|
|
24
25
|
}];
|
|
25
26
|
const result = formatAncestryChain(items);
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Both are in App, so second item shows element name not component name
|
|
28
|
+
expect(result).toBe(`App
|
|
29
|
+
└─ button#submit-btn`);
|
|
28
30
|
});
|
|
29
|
-
it("includes nth-child
|
|
31
|
+
it("includes nth-child with component name at boundary", () => {
|
|
30
32
|
const items = [{
|
|
31
33
|
elementName: "li",
|
|
32
34
|
componentName: "ListItem",
|
|
@@ -36,10 +38,11 @@ describe("formatAncestryChain", () => {
|
|
|
36
38
|
componentName: "List"
|
|
37
39
|
}];
|
|
38
40
|
const result = formatAncestryChain(items);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// Different components = boundaries
|
|
42
|
+
expect(result).toBe(`List
|
|
43
|
+
└─ ListItem:nth-child(3)`);
|
|
41
44
|
});
|
|
42
|
-
it("includes both nth-child and ID
|
|
45
|
+
it("includes both nth-child and ID at component boundary", () => {
|
|
43
46
|
const items = [{
|
|
44
47
|
elementName: "li",
|
|
45
48
|
componentName: "ListItem",
|
|
@@ -50,10 +53,10 @@ describe("formatAncestryChain", () => {
|
|
|
50
53
|
componentName: "List"
|
|
51
54
|
}];
|
|
52
55
|
const result = formatAncestryChain(items);
|
|
53
|
-
expect(result).toBe(`
|
|
54
|
-
└─
|
|
56
|
+
expect(result).toBe(`List
|
|
57
|
+
└─ ListItem:nth-child(2)#special-item`);
|
|
55
58
|
});
|
|
56
|
-
it("includes file location
|
|
59
|
+
it("includes file location at component boundary", () => {
|
|
57
60
|
const items = [{
|
|
58
61
|
elementName: "button",
|
|
59
62
|
componentName: "Button",
|
|
@@ -62,7 +65,8 @@ describe("formatAncestryChain", () => {
|
|
|
62
65
|
line: 42
|
|
63
66
|
}];
|
|
64
67
|
const result = formatAncestryChain(items);
|
|
65
|
-
|
|
68
|
+
// First item is always a boundary (no previous item)
|
|
69
|
+
expect(result).toBe("Button#save at src/Button.tsx:42");
|
|
66
70
|
});
|
|
67
71
|
it("formats element without component name", () => {
|
|
68
72
|
const items = [{
|
|
@@ -78,7 +82,7 @@ describe("formatAncestryChain", () => {
|
|
|
78
82
|
const result = formatAncestryChain([]);
|
|
79
83
|
expect(result).toBe("");
|
|
80
84
|
});
|
|
81
|
-
it("
|
|
85
|
+
it("uses innermost component as display name with outer components in chain", () => {
|
|
82
86
|
const items = [{
|
|
83
87
|
elementName: "div",
|
|
84
88
|
id: "sidebar-panel",
|
|
@@ -101,10 +105,11 @@ describe("formatAncestryChain", () => {
|
|
|
101
105
|
line: 104
|
|
102
106
|
}];
|
|
103
107
|
const result = formatAncestryChain(items);
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
// GlassPanel (innermost) is the display name, Sidebar (outer) shown in "in"
|
|
109
|
+
expect(result).toBe(`App at src/App.jsx:104
|
|
110
|
+
└─ GlassPanel#sidebar-panel in Sidebar at src/components/game/Sidebar.jsx:78`);
|
|
106
111
|
});
|
|
107
|
-
it("
|
|
112
|
+
it("uses component name as display name when only one in chain", () => {
|
|
108
113
|
const items = [{
|
|
109
114
|
elementName: "button",
|
|
110
115
|
componentName: "Button",
|
|
@@ -117,6 +122,7 @@ describe("formatAncestryChain", () => {
|
|
|
117
122
|
line: 10
|
|
118
123
|
}];
|
|
119
124
|
const result = formatAncestryChain(items);
|
|
120
|
-
|
|
125
|
+
// Single component becomes the display name, no "in X" needed
|
|
126
|
+
expect(result).toBe("Button at src/Button.tsx:10");
|
|
121
127
|
});
|
|
122
128
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert absolute file paths to relative paths for display.
|
|
3
|
+
*
|
|
4
|
+
* This ensures consistent path display across different sources:
|
|
5
|
+
* - React _debugSource may provide absolute paths
|
|
6
|
+
* - Next.js data-locatorjs attributes may have absolute paths
|
|
7
|
+
* - Phoenix comments use relative paths
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "/Users/name/project/src/App.tsx" → "src/App.tsx"
|
|
11
|
+
* - "/workspace/apps/next-16/app/page.tsx" → "app/page.tsx"
|
|
12
|
+
* - "src/components/Button.tsx" → "src/components/Button.tsx" (unchanged)
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeFilePath(filePath: string): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert absolute file paths to relative paths for display.
|
|
3
|
+
*
|
|
4
|
+
* This ensures consistent path display across different sources:
|
|
5
|
+
* - React _debugSource may provide absolute paths
|
|
6
|
+
* - Next.js data-locatorjs attributes may have absolute paths
|
|
7
|
+
* - Phoenix comments use relative paths
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "/Users/name/project/src/App.tsx" → "src/App.tsx"
|
|
11
|
+
* - "/workspace/apps/next-16/app/page.tsx" → "app/page.tsx"
|
|
12
|
+
* - "src/components/Button.tsx" → "src/components/Button.tsx" (unchanged)
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeFilePath(filePath) {
|
|
15
|
+
// If it's already relative (doesn't start with /), return as-is
|
|
16
|
+
if (!filePath.startsWith("/")) {
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Find common project indicators to trim the path
|
|
21
|
+
const indicators = ["/app/", "/src/", "/pages/", "/components/", "/lib/"];
|
|
22
|
+
for (const indicator of indicators) {
|
|
23
|
+
const index = filePath.indexOf(indicator);
|
|
24
|
+
if (index !== -1) {
|
|
25
|
+
// Return from the indicator onwards (remove leading slash)
|
|
26
|
+
return filePath.substring(index + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If no indicator found, try to get just the last few path segments
|
|
31
|
+
const parts = filePath.split("/");
|
|
32
|
+
|
|
33
|
+
// Return last 3-4 segments if available (e.g., "apps/next-16/app/page.tsx")
|
|
34
|
+
if (parts.length > 3) {
|
|
35
|
+
return parts.slice(-4).join("/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Last resort: return as-is
|
|
39
|
+
return filePath;
|
|
40
|
+
}
|
package/dist/output.css
CHANGED
|
@@ -554,7 +554,7 @@ video {
|
|
|
554
554
|
display: none;
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
-
[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
|
|
557
|
+
input:where([type='text']),input:where(:not([type])),input:where([type='email']),input:where([type='url']),input:where([type='password']),input:where([type='number']),input:where([type='date']),input:where([type='datetime-local']),input:where([type='month']),input:where([type='search']),input:where([type='tel']),input:where([type='time']),input:where([type='week']),select:where([multiple]),textarea,select {
|
|
558
558
|
-webkit-appearance: none;
|
|
559
559
|
-moz-appearance: none;
|
|
560
560
|
appearance: none;
|
|
@@ -571,7 +571,7 @@ video {
|
|
|
571
571
|
--tw-shadow: 0 0 #0000;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
|
|
574
|
+
input:where([type='text']):focus, input:where(:not([type])):focus, input:where([type='email']):focus, input:where([type='url']):focus, input:where([type='password']):focus, input:where([type='number']):focus, input:where([type='date']):focus, input:where([type='datetime-local']):focus, input:where([type='month']):focus, input:where([type='search']):focus, input:where([type='tel']):focus, input:where([type='time']):focus, input:where([type='week']):focus, select:where([multiple]):focus, textarea:focus, select:focus {
|
|
575
575
|
outline: 2px solid transparent;
|
|
576
576
|
outline-offset: 2px;
|
|
577
577
|
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
|
@@ -600,6 +600,11 @@ input::placeholder,textarea::placeholder {
|
|
|
600
600
|
|
|
601
601
|
::-webkit-date-and-time-value {
|
|
602
602
|
min-height: 1.5em;
|
|
603
|
+
text-align: inherit;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
::-webkit-datetime-edit {
|
|
607
|
+
display: inline-flex;
|
|
603
608
|
}
|
|
604
609
|
|
|
605
610
|
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
|
|
@@ -617,7 +622,7 @@ select {
|
|
|
617
622
|
print-color-adjust: exact;
|
|
618
623
|
}
|
|
619
624
|
|
|
620
|
-
[multiple],[size]:
|
|
625
|
+
select:where([multiple]),select:where([size]:not([size="1"])) {
|
|
621
626
|
background-image: initial;
|
|
622
627
|
background-position: initial;
|
|
623
628
|
background-repeat: unset;
|
|
@@ -627,7 +632,7 @@ select {
|
|
|
627
632
|
print-color-adjust: unset;
|
|
628
633
|
}
|
|
629
634
|
|
|
630
|
-
[type='checkbox'],[type='radio'] {
|
|
635
|
+
input:where([type='checkbox']),input:where([type='radio']) {
|
|
631
636
|
-webkit-appearance: none;
|
|
632
637
|
-moz-appearance: none;
|
|
633
638
|
appearance: none;
|
|
@@ -650,15 +655,15 @@ select {
|
|
|
650
655
|
--tw-shadow: 0 0 #0000;
|
|
651
656
|
}
|
|
652
657
|
|
|
653
|
-
[type='checkbox'] {
|
|
658
|
+
input:where([type='checkbox']) {
|
|
654
659
|
border-radius: 0px;
|
|
655
660
|
}
|
|
656
661
|
|
|
657
|
-
[type='radio'] {
|
|
662
|
+
input:where([type='radio']) {
|
|
658
663
|
border-radius: 100%;
|
|
659
664
|
}
|
|
660
665
|
|
|
661
|
-
[type='checkbox']:focus,[type='radio']:focus {
|
|
666
|
+
input:where([type='checkbox']):focus,input:where([type='radio']):focus {
|
|
662
667
|
outline: 2px solid transparent;
|
|
663
668
|
outline-offset: 2px;
|
|
664
669
|
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
|
@@ -670,7 +675,7 @@ select {
|
|
|
670
675
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
671
676
|
}
|
|
672
677
|
|
|
673
|
-
[type='checkbox']:checked,[type='radio']:checked {
|
|
678
|
+
input:where([type='checkbox']):checked,input:where([type='radio']):checked {
|
|
674
679
|
border-color: transparent;
|
|
675
680
|
background-color: currentColor;
|
|
676
681
|
background-size: 100% 100%;
|
|
@@ -678,20 +683,36 @@ select {
|
|
|
678
683
|
background-repeat: no-repeat;
|
|
679
684
|
}
|
|
680
685
|
|
|
681
|
-
[type='checkbox']:checked {
|
|
686
|
+
input:where([type='checkbox']):checked {
|
|
682
687
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
|
683
688
|
}
|
|
684
689
|
|
|
685
|
-
|
|
690
|
+
@media (forced-colors: active) {
|
|
691
|
+
input:where([type='checkbox']):checked {
|
|
692
|
+
-webkit-appearance: auto;
|
|
693
|
+
-moz-appearance: auto;
|
|
694
|
+
appearance: auto;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
input:where([type='radio']):checked {
|
|
686
699
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
|
|
687
700
|
}
|
|
688
701
|
|
|
689
|
-
|
|
702
|
+
@media (forced-colors: active) {
|
|
703
|
+
input:where([type='radio']):checked {
|
|
704
|
+
-webkit-appearance: auto;
|
|
705
|
+
-moz-appearance: auto;
|
|
706
|
+
appearance: auto;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
input:where([type='checkbox']):checked:hover,input:where([type='checkbox']):checked:focus,input:where([type='radio']):checked:hover,input:where([type='radio']):checked:focus {
|
|
690
711
|
border-color: transparent;
|
|
691
712
|
background-color: currentColor;
|
|
692
713
|
}
|
|
693
714
|
|
|
694
|
-
[type='checkbox']:indeterminate {
|
|
715
|
+
input:where([type='checkbox']):indeterminate {
|
|
695
716
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
|
|
696
717
|
border-color: transparent;
|
|
697
718
|
background-color: currentColor;
|
|
@@ -700,12 +721,20 @@ select {
|
|
|
700
721
|
background-repeat: no-repeat;
|
|
701
722
|
}
|
|
702
723
|
|
|
703
|
-
|
|
724
|
+
@media (forced-colors: active) {
|
|
725
|
+
input:where([type='checkbox']):indeterminate {
|
|
726
|
+
-webkit-appearance: auto;
|
|
727
|
+
-moz-appearance: auto;
|
|
728
|
+
appearance: auto;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
input:where([type='checkbox']):indeterminate:hover,input:where([type='checkbox']):indeterminate:focus {
|
|
704
733
|
border-color: transparent;
|
|
705
734
|
background-color: currentColor;
|
|
706
735
|
}
|
|
707
736
|
|
|
708
|
-
[type='file'] {
|
|
737
|
+
input:where([type='file']) {
|
|
709
738
|
background: unset;
|
|
710
739
|
border-color: inherit;
|
|
711
740
|
border-width: 0;
|
|
@@ -715,11 +744,45 @@ select {
|
|
|
715
744
|
line-height: inherit;
|
|
716
745
|
}
|
|
717
746
|
|
|
718
|
-
[type='file']:focus {
|
|
747
|
+
input:where([type='file']):focus {
|
|
719
748
|
outline: 1px solid ButtonText;
|
|
720
749
|
outline: 1px auto -webkit-focus-ring-color;
|
|
721
750
|
}
|
|
722
751
|
|
|
752
|
+
.container {
|
|
753
|
+
width: 100%;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
@media (min-width: 640px) {
|
|
757
|
+
.container {
|
|
758
|
+
max-width: 640px;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
@media (min-width: 768px) {
|
|
763
|
+
.container {
|
|
764
|
+
max-width: 768px;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
@media (min-width: 1024px) {
|
|
769
|
+
.container {
|
|
770
|
+
max-width: 1024px;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
@media (min-width: 1280px) {
|
|
775
|
+
.container {
|
|
776
|
+
max-width: 1280px;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
@media (min-width: 1536px) {
|
|
781
|
+
.container {
|
|
782
|
+
max-width: 1536px;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
723
786
|
.sr-only {
|
|
724
787
|
position: absolute;
|
|
725
788
|
width: 1px;
|
|
@@ -1184,6 +1247,11 @@ select {
|
|
|
1184
1247
|
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
|
1185
1248
|
}
|
|
1186
1249
|
|
|
1250
|
+
.border-gray-500 {
|
|
1251
|
+
--tw-border-opacity: 1;
|
|
1252
|
+
border-color: rgb(107 114 128 / var(--tw-border-opacity, 1));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1187
1255
|
.border-gray-600 {
|
|
1188
1256
|
--tw-border-opacity: 1;
|
|
1189
1257
|
border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
|
|
@@ -1351,6 +1419,10 @@ select {
|
|
|
1351
1419
|
padding: 1rem;
|
|
1352
1420
|
}
|
|
1353
1421
|
|
|
1422
|
+
.p-5 {
|
|
1423
|
+
padding: 1.25rem;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1354
1426
|
.p-6 {
|
|
1355
1427
|
padding: 1.5rem;
|
|
1356
1428
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a server-side component in the ancestry chain.
|
|
3
|
+
* Used for Phoenix LiveView, Rails ViewComponents, Next.js RSC, etc.
|
|
4
|
+
*/
|
|
5
|
+
export interface ServerComponentInfo {
|
|
6
|
+
/** Component name (e.g., "AppWeb.CoreComponents.button") or "@caller" for call site */
|
|
7
|
+
name: string;
|
|
8
|
+
/** File path (e.g., "lib/app_web/core_components.ex") */
|
|
9
|
+
filePath: string;
|
|
10
|
+
/** Line number in the source file */
|
|
11
|
+
line: number;
|
|
12
|
+
/** Type of server component annotation */
|
|
13
|
+
type: "component" | "caller";
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treelocator/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@babel/cli": "^7.25.9",
|
|
55
55
|
"@babel/core": "^7.26.0",
|
|
56
|
-
"@
|
|
56
|
+
"@tailwindcss/forms": "^0.5.11",
|
|
57
|
+
"@treelocator/dev-config": "^0.2.0",
|
|
57
58
|
"@types/jsdom": "^21.1.7",
|
|
58
59
|
"babel-preset-solid": "^1.9.2",
|
|
59
60
|
"concurrently": "^9.1.0",
|
|
@@ -72,5 +73,5 @@
|
|
|
72
73
|
"directory": "packages/runtime"
|
|
73
74
|
},
|
|
74
75
|
"license": "MIT",
|
|
75
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "5d53daa18f4fef5e815c3fd281b899608f8673ea"
|
|
76
77
|
}
|
|
@@ -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[];
|
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
import { TreeNode } from "../types/TreeNode";
|
|
2
2
|
import { ReactTreeNodeElement } from "./react/reactAdapter";
|
|
3
3
|
import { JSXTreeNodeElement } from "./jsx/jsxAdapter";
|
|
4
|
+
import { SvelteTreeNodeElement } from "./svelte/svelteAdapter";
|
|
5
|
+
import { VueTreeNodeElement } from "./vue/vueAdapter";
|
|
4
6
|
import {
|
|
5
7
|
detectJSX,
|
|
6
8
|
detectReact,
|
|
9
|
+
detectSvelte,
|
|
10
|
+
detectVue,
|
|
7
11
|
} from "@locator/shared";
|
|
12
|
+
import { detectPhoenix } from "./phoenix/detectPhoenix";
|
|
8
13
|
|
|
9
14
|
export function createTreeNode(
|
|
10
15
|
element: HTMLElement,
|
|
11
16
|
adapterId?: string
|
|
12
17
|
): TreeNode | null {
|
|
13
|
-
// Check for
|
|
14
|
-
if (adapterId === "react"
|
|
18
|
+
// Check for explicit adapter ID first
|
|
19
|
+
if (adapterId === "react") {
|
|
20
|
+
return new ReactTreeNodeElement(element);
|
|
21
|
+
}
|
|
22
|
+
if (adapterId === "svelte") {
|
|
23
|
+
return new SvelteTreeNodeElement(element);
|
|
24
|
+
}
|
|
25
|
+
if (adapterId === "vue") {
|
|
26
|
+
return new VueTreeNodeElement(element);
|
|
27
|
+
}
|
|
28
|
+
if (adapterId === "jsx") {
|
|
29
|
+
return new JSXTreeNodeElement(element);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Auto-detect framework
|
|
33
|
+
if (detectSvelte()) {
|
|
34
|
+
return new SvelteTreeNodeElement(element);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (detectVue()) {
|
|
38
|
+
return new VueTreeNodeElement(element);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (detectReact()) {
|
|
15
42
|
return new ReactTreeNodeElement(element);
|
|
16
43
|
}
|
|
17
44
|
|
|
18
45
|
// Check for JSX adapter (babel plugin) - check if element has data-locatorjs-id
|
|
19
|
-
if (
|
|
46
|
+
if (detectJSX() || element.dataset.locatorjsId) {
|
|
47
|
+
return new JSXTreeNodeElement(element);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for Phoenix LiveView (uses JSX adapter as fallback for pure Phoenix apps)
|
|
51
|
+
if (detectPhoenix()) {
|
|
20
52
|
return new JSXTreeNodeElement(element);
|
|
21
53
|
}
|
|
22
54
|
|