@treelocator/runtime 0.1.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 (227) hide show
  1. package/.eslintrc +3 -0
  2. package/.turbo/turbo-build.log +30 -0
  3. package/.turbo/turbo-test.log +18 -0
  4. package/.turbo/turbo-ts.log +4 -0
  5. package/LICENSE +22 -0
  6. package/babel.config.js +14 -0
  7. package/dist/_generated_styles.d.ts +2 -0
  8. package/dist/_generated_styles.js +1734 -0
  9. package/dist/_generated_tree_icon.d.ts +2 -0
  10. package/dist/_generated_tree_icon.js +2 -0
  11. package/dist/adapters/HtmlElementTreeNode.d.ts +16 -0
  12. package/dist/adapters/HtmlElementTreeNode.js +43 -0
  13. package/dist/adapters/adapterApi.d.ts +30 -0
  14. package/dist/adapters/adapterApi.js +1 -0
  15. package/dist/adapters/createTreeNode.d.ts +2 -0
  16. package/dist/adapters/createTreeNode.js +17 -0
  17. package/dist/adapters/getElementInfo.d.ts +2 -0
  18. package/dist/adapters/getElementInfo.js +19 -0
  19. package/dist/adapters/getParentsPath.d.ts +2 -0
  20. package/dist/adapters/getParentsPath.js +35 -0
  21. package/dist/adapters/getTree.d.ts +1 -0
  22. package/dist/adapters/getTree.js +35 -0
  23. package/dist/adapters/goUpByTheTree.d.ts +7 -0
  24. package/dist/adapters/goUpByTheTree.js +22 -0
  25. package/dist/adapters/jsx/getExpressionData.d.ts +2 -0
  26. package/dist/adapters/jsx/getExpressionData.js +44 -0
  27. package/dist/adapters/jsx/getJSXComponentBoundingBox.d.ts +5 -0
  28. package/dist/adapters/jsx/getJSXComponentBoundingBox.js +46 -0
  29. package/dist/adapters/jsx/jsxAdapter.d.ts +11 -0
  30. package/dist/adapters/jsx/jsxAdapter.js +208 -0
  31. package/dist/adapters/jsx/runtimeStore.d.ts +10 -0
  32. package/dist/adapters/jsx/runtimeStore.js +87 -0
  33. package/dist/adapters/react/fiberToSimple.d.ts +3 -0
  34. package/dist/adapters/react/fiberToSimple.js +55 -0
  35. package/dist/adapters/react/findDebugSource.d.ts +5 -0
  36. package/dist/adapters/react/findDebugSource.js +13 -0
  37. package/dist/adapters/react/findFiberByHtmlElement.d.ts +2 -0
  38. package/dist/adapters/react/findFiberByHtmlElement.js +22 -0
  39. package/dist/adapters/react/gatherFiberRoots.d.ts +2 -0
  40. package/dist/adapters/react/gatherFiberRoots.js +29 -0
  41. package/dist/adapters/react/getAllFiberChildren.d.ts +2 -0
  42. package/dist/adapters/react/getAllFiberChildren.js +9 -0
  43. package/dist/adapters/react/getAllParentsElementsAndRootComponent.d.ts +8 -0
  44. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +34 -0
  45. package/dist/adapters/react/getAllWrappingParents.d.ts +2 -0
  46. package/dist/adapters/react/getAllWrappingParents.js +19 -0
  47. package/dist/adapters/react/getFiberComponentBoundingBox.d.ts +3 -0
  48. package/dist/adapters/react/getFiberComponentBoundingBox.js +27 -0
  49. package/dist/adapters/react/getFiberLabel.d.ts +3 -0
  50. package/dist/adapters/react/getFiberLabel.js +14 -0
  51. package/dist/adapters/react/getFiberOwnBoundingBox.d.ts +3 -0
  52. package/dist/adapters/react/getFiberOwnBoundingBox.js +6 -0
  53. package/dist/adapters/react/isStyled.d.ts +2 -0
  54. package/dist/adapters/react/isStyled.js +3 -0
  55. package/dist/adapters/react/makeFiberId.d.ts +2 -0
  56. package/dist/adapters/react/makeFiberId.js +16 -0
  57. package/dist/adapters/react/reactAdapter.d.ts +11 -0
  58. package/dist/adapters/react/reactAdapter.js +114 -0
  59. package/dist/adapters/react/searchDevtoolsRenderersForClosestTarget.d.ts +1 -0
  60. package/dist/adapters/react/searchDevtoolsRenderersForClosestTarget.js +11 -0
  61. package/dist/adapters/svelte/svelteAdapter.d.ts +22 -0
  62. package/dist/adapters/svelte/svelteAdapter.js +88 -0
  63. package/dist/adapters/vue/getVNodeBoundingBox.d.ts +4 -0
  64. package/dist/adapters/vue/getVNodeBoundingBox.js +31 -0
  65. package/dist/adapters/vue/vueAdapter.d.ts +15 -0
  66. package/dist/adapters/vue/vueAdapter.js +113 -0
  67. package/dist/browserApi.d.ts +103 -0
  68. package/dist/browserApi.js +160 -0
  69. package/dist/components/Button.d.ts +4 -0
  70. package/dist/components/Button.js +17 -0
  71. package/dist/components/ComponentOutline.d.ts +7 -0
  72. package/dist/components/ComponentOutline.js +93 -0
  73. package/dist/components/MaybeOutline.d.ts +7 -0
  74. package/dist/components/MaybeOutline.js +43 -0
  75. package/dist/components/Outline.d.ts +25 -0
  76. package/dist/components/Outline.js +135 -0
  77. package/dist/components/RenderBoxes.d.ts +4 -0
  78. package/dist/components/RenderBoxes.js +73 -0
  79. package/dist/components/Runtime.d.ts +3 -0
  80. package/dist/components/Runtime.js +188 -0
  81. package/dist/components/SimpleNodeOutline.d.ts +4 -0
  82. package/dist/components/SimpleNodeOutline.js +47 -0
  83. package/dist/components/Toast.d.ts +4 -0
  84. package/dist/components/Toast.js +68 -0
  85. package/dist/components/Tooltip.d.ts +5 -0
  86. package/dist/components/Tooltip.js +21 -0
  87. package/dist/consts.d.ts +6 -0
  88. package/dist/consts.js +5 -0
  89. package/dist/functions/cropPath.d.ts +1 -0
  90. package/dist/functions/cropPath.js +9 -0
  91. package/dist/functions/cropPath.test.d.ts +1 -0
  92. package/dist/functions/cropPath.test.js +16 -0
  93. package/dist/functions/deduplicateLabels.d.ts +2 -0
  94. package/dist/functions/deduplicateLabels.js +12 -0
  95. package/dist/functions/evalTemplate.d.ts +3 -0
  96. package/dist/functions/evalTemplate.js +7 -0
  97. package/dist/functions/evalTemplate.test.d.ts +1 -0
  98. package/dist/functions/evalTemplate.test.js +11 -0
  99. package/dist/functions/findNames.d.ts +5 -0
  100. package/dist/functions/findNames.js +15 -0
  101. package/dist/functions/formatAncestryChain.d.ts +9 -0
  102. package/dist/functions/formatAncestryChain.js +56 -0
  103. package/dist/functions/getBoundingRect.d.ts +1 -0
  104. package/dist/functions/getBoundingRect.js +11 -0
  105. package/dist/functions/getComposedBoundingBox.d.ts +2 -0
  106. package/dist/functions/getComposedBoundingBox.js +20 -0
  107. package/dist/functions/getIdsOnPathToRoot.d.ts +3 -0
  108. package/dist/functions/getIdsOnPathToRoot.js +15 -0
  109. package/dist/functions/getMultipleElementsBoundingBox.d.ts +2 -0
  110. package/dist/functions/getMultipleElementsBoundingBox.js +20 -0
  111. package/dist/functions/getPathToParent.d.ts +1 -0
  112. package/dist/functions/getPathToParent.js +15 -0
  113. package/dist/functions/getReferenceId.d.ts +1 -0
  114. package/dist/functions/getReferenceId.js +9 -0
  115. package/dist/functions/getUsableFileName.d.ts +1 -0
  116. package/dist/functions/getUsableFileName.js +17 -0
  117. package/dist/functions/getUsableFileName.test.d.ts +1 -0
  118. package/dist/functions/getUsableFileName.test.js +16 -0
  119. package/dist/functions/getUsableName.d.ts +2 -0
  120. package/dist/functions/getUsableName.js +47 -0
  121. package/dist/functions/isCombinationModifiersPressed.d.ts +4 -0
  122. package/dist/functions/isCombinationModifiersPressed.js +16 -0
  123. package/dist/functions/isLocatorsOwnElement.d.ts +1 -0
  124. package/dist/functions/isLocatorsOwnElement.js +3 -0
  125. package/dist/functions/mergeRects.d.ts +2 -0
  126. package/dist/functions/mergeRects.js +10 -0
  127. package/dist/functions/mergeRects.test.d.ts +1 -0
  128. package/dist/functions/mergeRects.test.js +23 -0
  129. package/dist/functions/nonNullable.d.ts +1 -0
  130. package/dist/functions/nonNullable.js +3 -0
  131. package/dist/functions/parseDataId.d.ts +3 -0
  132. package/dist/functions/parseDataId.js +44 -0
  133. package/dist/functions/transformPath.d.ts +1 -0
  134. package/dist/functions/transformPath.js +7 -0
  135. package/dist/functions/transformPath.test.d.ts +1 -0
  136. package/dist/functions/transformPath.test.js +16 -0
  137. package/dist/global.d.js +1 -0
  138. package/dist/index.d.ts +11 -0
  139. package/dist/index.js +13 -0
  140. package/dist/initRuntime.d.ts +8 -0
  141. package/dist/initRuntime.js +80 -0
  142. package/dist/output.css +1733 -0
  143. package/dist/types/LabelData.d.ts +5 -0
  144. package/dist/types/LabelData.js +1 -0
  145. package/dist/types/TreeNode.d.ts +19 -0
  146. package/dist/types/TreeNode.js +1 -0
  147. package/dist/types/types.d.ts +53 -0
  148. package/dist/types/types.js +1 -0
  149. package/jest.config.ts +195 -0
  150. package/package.json +75 -0
  151. package/scripts/wrapCSS.js +26 -0
  152. package/scripts/wrapImage.js +24 -0
  153. package/src/_generated_styles.ts +1734 -0
  154. package/src/_generated_tree_icon.ts +2 -0
  155. package/src/adapters/HtmlElementTreeNode.ts +51 -0
  156. package/src/adapters/adapterApi.ts +35 -0
  157. package/src/adapters/createTreeNode.ts +25 -0
  158. package/src/adapters/getElementInfo.tsx +27 -0
  159. package/src/adapters/getParentsPath.tsx +49 -0
  160. package/src/adapters/getTree.tsx +45 -0
  161. package/src/adapters/goUpByTheTree.ts +20 -0
  162. package/src/adapters/jsx/getExpressionData.ts +47 -0
  163. package/src/adapters/jsx/getJSXComponentBoundingBox.ts +63 -0
  164. package/src/adapters/jsx/jsxAdapter.ts +276 -0
  165. package/src/adapters/jsx/runtimeStore.ts +94 -0
  166. package/src/adapters/react/fiberToSimple.tsx +72 -0
  167. package/src/adapters/react/findDebugSource.ts +15 -0
  168. package/src/adapters/react/findFiberByHtmlElement.ts +27 -0
  169. package/src/adapters/react/gatherFiberRoots.tsx +36 -0
  170. package/src/adapters/react/getAllFiberChildren.tsx +11 -0
  171. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +52 -0
  172. package/src/adapters/react/getAllWrappingParents.ts +25 -0
  173. package/src/adapters/react/getFiberComponentBoundingBox.ts +30 -0
  174. package/src/adapters/react/getFiberLabel.ts +20 -0
  175. package/src/adapters/react/getFiberOwnBoundingBox.ts +9 -0
  176. package/src/adapters/react/isStyled.ts +5 -0
  177. package/src/adapters/react/makeFiberId.tsx +19 -0
  178. package/src/adapters/react/reactAdapter.ts +148 -0
  179. package/src/adapters/react/searchDevtoolsRenderersForClosestTarget.ts +15 -0
  180. package/src/adapters/svelte/svelteAdapter.ts +111 -0
  181. package/src/adapters/vue/getVNodeBoundingBox.tsx +42 -0
  182. package/src/adapters/vue/vueAdapter.ts +139 -0
  183. package/src/assets/tree-icon.png +0 -0
  184. package/src/browserApi.ts +288 -0
  185. package/src/components/Button.tsx +14 -0
  186. package/src/components/ComponentOutline.tsx +98 -0
  187. package/src/components/MaybeOutline.tsx +49 -0
  188. package/src/components/Outline.tsx +153 -0
  189. package/src/components/RenderBoxes.tsx +57 -0
  190. package/src/components/Runtime.tsx +246 -0
  191. package/src/components/SimpleNodeOutline.tsx +27 -0
  192. package/src/components/Toast.tsx +83 -0
  193. package/src/components/Tooltip.tsx +28 -0
  194. package/src/consts.ts +7 -0
  195. package/src/functions/cropPath.test.ts +18 -0
  196. package/src/functions/cropPath.ts +12 -0
  197. package/src/functions/deduplicateLabels.ts +16 -0
  198. package/src/functions/evalTemplate.test.ts +12 -0
  199. package/src/functions/evalTemplate.ts +8 -0
  200. package/src/functions/findNames.ts +20 -0
  201. package/src/functions/formatAncestryChain.ts +80 -0
  202. package/src/functions/getBoundingRect.tsx +11 -0
  203. package/src/functions/getComposedBoundingBox.tsx +25 -0
  204. package/src/functions/getIdsOnPathToRoot.tsx +21 -0
  205. package/src/functions/getMultipleElementsBoundingBox.tsx +25 -0
  206. package/src/functions/getPathToParent.tsx +17 -0
  207. package/src/functions/getReferenceId.tsx +10 -0
  208. package/src/functions/getUsableFileName.test.tsx +24 -0
  209. package/src/functions/getUsableFileName.tsx +19 -0
  210. package/src/functions/getUsableName.ts +52 -0
  211. package/src/functions/isCombinationModifiersPressed.ts +32 -0
  212. package/src/functions/isLocatorsOwnElement.tsx +9 -0
  213. package/src/functions/mergeRects.test.ts +15 -0
  214. package/src/functions/mergeRects.tsx +12 -0
  215. package/src/functions/nonNullable.ts +3 -0
  216. package/src/functions/parseDataId.ts +62 -0
  217. package/src/functions/transformPath.test.ts +28 -0
  218. package/src/functions/transformPath.ts +7 -0
  219. package/src/global.d.ts +31 -0
  220. package/src/index.ts +18 -0
  221. package/src/initRuntime.ts +83 -0
  222. package/src/main.css +3 -0
  223. package/src/types/LabelData.ts +6 -0
  224. package/src/types/TreeNode.ts +22 -0
  225. package/src/types/types.ts +55 -0
  226. package/tailwind.config.js +9 -0
  227. package/tsconfig.json +20 -0
@@ -0,0 +1,246 @@
1
+ import { Targets } from "@locator/shared";
2
+ import { batch, createEffect, createSignal, onCleanup } from "solid-js";
3
+ import { render } from "solid-js/web";
4
+ import { AdapterId } from "../consts";
5
+ import { isCombinationModifiersPressed } from "../functions/isCombinationModifiersPressed";
6
+ import { Targets as SetupTargets } from "../types/types";
7
+ import { MaybeOutline } from "./MaybeOutline";
8
+ import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
9
+ import { Toast } from "./Toast";
10
+ import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
11
+ import { createTreeNode } from "../adapters/createTreeNode";
12
+ import treeIconUrl from "../_generated_tree_icon";
13
+
14
+ type RuntimeProps = {
15
+ adapterId?: AdapterId;
16
+ targets: Targets;
17
+ };
18
+
19
+ function Runtime(props: RuntimeProps) {
20
+ const [holdingModKey, setHoldingModKey] = createSignal<boolean>(false);
21
+ const [currentElement, setCurrentElement] = createSignal<HTMLElement | null>(
22
+ null
23
+ );
24
+ const [toastMessage, setToastMessage] = createSignal<string | null>(null);
25
+ const [locatorActive, setLocatorActive] = createSignal<boolean>(false);
26
+
27
+ const isActive = () => (holdingModKey() || locatorActive()) && currentElement();
28
+
29
+ createEffect(() => {
30
+ if (isActive()) {
31
+ document.body.classList.add("locatorjs-active-pointer");
32
+ } else {
33
+ document.body.classList.remove("locatorjs-active-pointer");
34
+ }
35
+ });
36
+
37
+ function keyUpListener(e: KeyboardEvent) {
38
+ setHoldingModKey(isCombinationModifiersPressed(e));
39
+ }
40
+
41
+ function keyDownListener(e: KeyboardEvent) {
42
+ setHoldingModKey(isCombinationModifiersPressed(e, true));
43
+ }
44
+
45
+ function mouseOverListener(e: MouseEvent) {
46
+ const target = e.target;
47
+ if (target && target instanceof HTMLElement) {
48
+ if (isLocatorsOwnElement(target)) {
49
+ return;
50
+ }
51
+
52
+ setHoldingModKey(isCombinationModifiersPressed(e, true));
53
+
54
+ batch(() => {
55
+ setCurrentElement(target);
56
+ });
57
+ }
58
+ }
59
+
60
+ function mouseDownUpListener(e: MouseEvent) {
61
+ if (isCombinationModifiersPressed(e) || locatorActive()) {
62
+ e.preventDefault();
63
+ e.stopPropagation();
64
+ }
65
+ }
66
+
67
+ function clickListener(e: MouseEvent) {
68
+ if (!isCombinationModifiersPressed(e) && !locatorActive()) {
69
+ return;
70
+ }
71
+
72
+ const target = e.target;
73
+ if (target && target instanceof HTMLElement) {
74
+ if (target.shadowRoot) {
75
+ return;
76
+ }
77
+
78
+ if (isLocatorsOwnElement(target)) {
79
+ return;
80
+ }
81
+
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+
85
+ // Copy ancestry to clipboard on alt+click
86
+ const treeNode = createTreeNode(target, props.adapterId);
87
+ if (treeNode) {
88
+ const ancestry = collectAncestry(treeNode);
89
+ const formatted = formatAncestryChain(ancestry);
90
+ navigator.clipboard.writeText(formatted).then(() => {
91
+ setToastMessage("Copied to clipboard");
92
+ });
93
+ }
94
+
95
+ // Deactivate toggle after click
96
+ if (locatorActive()) {
97
+ setLocatorActive(false);
98
+ }
99
+ }
100
+ }
101
+
102
+ function scrollListener() {
103
+ setCurrentElement(null);
104
+ }
105
+
106
+ const roots: (Document | ShadowRoot)[] = [document];
107
+ document.querySelectorAll("*").forEach((node) => {
108
+ if (node.id === "locatorjs-wrapper") {
109
+ return;
110
+ }
111
+ if (node.shadowRoot) {
112
+ roots.push(node.shadowRoot);
113
+ }
114
+ });
115
+
116
+ for (const root of roots) {
117
+ root.addEventListener("mouseover", mouseOverListener as EventListener, {
118
+ capture: true,
119
+ });
120
+ root.addEventListener("keydown", keyDownListener as EventListener);
121
+ root.addEventListener("keyup", keyUpListener as EventListener);
122
+ root.addEventListener("click", clickListener as EventListener, {
123
+ capture: true,
124
+ });
125
+ root.addEventListener("mousedown", mouseDownUpListener as EventListener, {
126
+ capture: true,
127
+ });
128
+ root.addEventListener("mouseup", mouseDownUpListener as EventListener, {
129
+ capture: true,
130
+ });
131
+ root.addEventListener("scroll", scrollListener);
132
+ }
133
+
134
+ onCleanup(() => {
135
+ for (const root of roots) {
136
+ root.removeEventListener("keyup", keyUpListener as EventListener);
137
+ root.removeEventListener("keydown", keyDownListener as EventListener);
138
+ root.removeEventListener(
139
+ "mouseover",
140
+ mouseOverListener as EventListener,
141
+ { capture: true }
142
+ );
143
+ root.removeEventListener("click", clickListener as EventListener, {
144
+ capture: true,
145
+ });
146
+ root.removeEventListener(
147
+ "mousedown",
148
+ mouseDownUpListener as EventListener,
149
+ { capture: true }
150
+ );
151
+ root.removeEventListener(
152
+ "mouseup",
153
+ mouseDownUpListener as EventListener,
154
+ { capture: true }
155
+ );
156
+ root.removeEventListener("scroll", scrollListener);
157
+ }
158
+ });
159
+
160
+ return (
161
+ <>
162
+ {isActive() ? (
163
+ <MaybeOutline
164
+ currentElement={currentElement()!}
165
+ adapterId={props.adapterId}
166
+ targets={props.targets}
167
+ />
168
+ ) : null}
169
+ {toastMessage() && (
170
+ <Toast
171
+ message={toastMessage()!}
172
+ onClose={() => setToastMessage(null)}
173
+ />
174
+ )}
175
+ <div
176
+ class="fixed pointer-events-auto"
177
+ style={{ "z-index": "2147483646", bottom: "20px", right: "20px" }}
178
+ title="LocatorJS - Component Ancestry Tracker"
179
+ data-locatorjs-api="window.__locatorjs__"
180
+ data-locatorjs-help="window.__locatorjs__.help()"
181
+ >
182
+ <div
183
+ class="rounded-full bg-white shadow-lg flex items-center justify-center cursor-pointer overflow-hidden"
184
+ style={{
185
+ width: "54px",
186
+ height: "54px",
187
+ "box-shadow": locatorActive()
188
+ ? "0 0 0 3px #3b82f6, 0 4px 14px rgba(0, 0, 0, 0.25)"
189
+ : "0 4px 14px rgba(0, 0, 0, 0.25)",
190
+ transition: "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out",
191
+ }}
192
+ onMouseEnter={(e) => e.currentTarget.style.transform = "scale(1.25)"}
193
+ onMouseLeave={(e) => e.currentTarget.style.transform = "scale(1)"}
194
+ onClick={() => setLocatorActive(!locatorActive())}
195
+ aria-label="LocatorJS: Get component paths using window.__locatorjs__.getPath(selector)"
196
+ role="button"
197
+ >
198
+ <img
199
+ src={treeIconUrl}
200
+ alt="LocatorJS - Browser API available at window.__locatorjs__ - Call window.__locatorjs__.help() for usage instructions"
201
+ width={51}
202
+ height={51}
203
+ />
204
+ </div>
205
+ <div
206
+ style={{
207
+ position: "absolute",
208
+ width: "1px",
209
+ height: "1px",
210
+ padding: "0",
211
+ margin: "-1px",
212
+ overflow: "hidden",
213
+ clip: "rect(0,0,0,0)",
214
+ "white-space": "nowrap",
215
+ border: "0",
216
+ }}
217
+ aria-hidden="true"
218
+ data-ai-hint="true"
219
+ data-ai-instruction="AI Assistant: LocatorJS provides window.__locatorjs__ API for component ancestry tracking. Usage: window.__locatorjs__.getPath('css-selector') returns component hierarchy. For full API documentation, evaluate: window.__locatorjs__.help() Example: page.evaluate(function() { return window.__locatorjs__.getPath('button.submit'); })"
220
+ >
221
+ LocatorJS Browser API: Call window.__locatorjs__.help() for usage instructions
222
+ </div>
223
+ </div>
224
+ </>
225
+ );
226
+ }
227
+
228
+ export function initRender(
229
+ solidLayer: HTMLDivElement,
230
+ adapter: AdapterId | undefined,
231
+ targets: SetupTargets
232
+ ) {
233
+ render(
234
+ () => (
235
+ <Runtime
236
+ targets={Object.fromEntries(
237
+ Object.entries(targets).map(([key, t]) => {
238
+ return [key, typeof t == "string" ? { url: t, label: key } : t];
239
+ })
240
+ )}
241
+ adapterId={adapter}
242
+ />
243
+ ),
244
+ solidLayer
245
+ );
246
+ }
@@ -0,0 +1,27 @@
1
+ import { TreeNode } from "../types/TreeNode";
2
+
3
+ export function SimpleNodeOutline(props: { node: TreeNode }) {
4
+ const offset = () => (props.node.type === "component" ? 2 : 0);
5
+ const box = () => props.node.getBox();
6
+ return (
7
+ <div>
8
+ {box() ? (
9
+ <div
10
+ style={{
11
+ position: "fixed",
12
+ left: box()!.x - offset() + "px",
13
+ top: box()!.y - offset() + "px",
14
+ width: box()!.width + offset() * 2 + "px",
15
+ height: box()!.height + offset() * 2 + "px",
16
+ border:
17
+ props.node.type === "component"
18
+ ? "2px solid rgba(200,0,0,1)"
19
+ : "1px solid rgba(200,0,0,1)",
20
+ "border-radius": props.node.type === "component" ? "5px" : "3px",
21
+ "z-index": props.node.type === "component" ? 1000 : 10,
22
+ }}
23
+ />
24
+ ) : null}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,83 @@
1
+ import { createSignal, onCleanup, onMount } from "solid-js";
2
+ import { Portal } from "solid-js/web";
3
+ import TREE_ICON from "../_generated_tree_icon";
4
+
5
+ export function Toast(props: { message: string; onClose: () => void }) {
6
+ let timeoutId: ReturnType<typeof setTimeout>;
7
+ const [flashOpacity, setFlashOpacity] = createSignal(1);
8
+
9
+ onMount(() => {
10
+ // Start flash fade after a brief moment so it's visible
11
+ setTimeout(() => {
12
+ setFlashOpacity(0);
13
+ }, 100);
14
+
15
+ timeoutId = setTimeout(() => {
16
+ props.onClose();
17
+ }, 1500);
18
+ });
19
+
20
+ onCleanup(() => {
21
+ clearTimeout(timeoutId);
22
+ });
23
+
24
+ return (
25
+ <Portal mount={document.body}>
26
+ {/* Flash overlay */}
27
+ <div
28
+ style={{
29
+ position: "fixed",
30
+ top: "0",
31
+ left: "0",
32
+ right: "0",
33
+ bottom: "0",
34
+ width: "100vw",
35
+ height: "100vh",
36
+ "z-index": 99999,
37
+ "background-color": "rgba(56, 189, 248, 0.5)",
38
+ opacity: flashOpacity(),
39
+ transition: "opacity 0.3s ease-out",
40
+ "pointer-events": "none",
41
+ }}
42
+ />
43
+ {/* Tree icon in bottom left */}
44
+ <div
45
+ style={{
46
+ position: "fixed",
47
+ bottom: "16px",
48
+ left: "16px",
49
+ "z-index": 100000,
50
+ "pointer-events": "none",
51
+ }}
52
+ >
53
+ <img
54
+ src={TREE_ICON}
55
+ alt="Tree"
56
+ style={{
57
+ width: "32px",
58
+ height: "32px",
59
+ }}
60
+ />
61
+ </div>
62
+ {/* Toast message */}
63
+ <div
64
+ style={{
65
+ position: "fixed",
66
+ bottom: "16px",
67
+ left: "50%",
68
+ transform: "translateX(-50%)",
69
+ "background-color": "#111827",
70
+ color: "white",
71
+ padding: "8px 16px",
72
+ "border-radius": "8px",
73
+ "box-shadow": "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
74
+ "font-size": "14px",
75
+ "z-index": 100000,
76
+ "pointer-events": "auto",
77
+ }}
78
+ >
79
+ {props.message}
80
+ </div>
81
+ </Portal>
82
+ );
83
+ }
@@ -0,0 +1,28 @@
1
+ export default function Tooltip(props: {
2
+ tooltipText?: string;
3
+ position?: "bottom" | "left" | "right" | "top";
4
+ children: any;
5
+ }) {
6
+ const positionMap = {
7
+ bottom: "-bottom-7 left-1/2 -translate-x-1/2 transform",
8
+ left: "-left-2 top-1/2 -translate-y-1/2 -translate-x-full transform",
9
+ right: "-right-2 top-1/2 -translate-y-1/2 translate-x-full transform",
10
+ top: "-top-7 left-1/2 -translate-x-1/2 transform",
11
+ };
12
+
13
+ return (
14
+ <div class="group/tooltip relative block">
15
+ {props.children}
16
+ <div
17
+ class={
18
+ "pointer-events-none invisible absolute z-10 whitespace-nowrap rounded bg-gray-700 px-2 py-1 text-center text-xs text-white opacity-0 transition-opacity duration-300 group-hover/tooltip:visible group-hover/tooltip:opacity-100" +
19
+ " " +
20
+ positionMap[props.position || "top"]
21
+ }
22
+ role="tooltip"
23
+ >
24
+ {props.tooltipText}
25
+ </div>
26
+ </div>
27
+ );
28
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const HREF_TARGET = "_self";
2
+ export const PADDING = 6;
3
+ export const baseColor = "#e90139";
4
+ export const hoverColor = "#C70139";
5
+ export const fontFamily = "Helvetica, sans-serif, Arial";
6
+
7
+ export type AdapterId = "react" | "jsx" | "svelte" | "vue";
@@ -0,0 +1,18 @@
1
+ import cropPath from "./cropPath";
2
+ import { describe, expect, test } from "vitest";
3
+
4
+ describe("cropPath", () => {
5
+ test("crop", () => {
6
+ expect(cropPath("aaa/bbb/ccc/ddd.tsx")).toBe(".../bbb/ccc/ddd.tsx");
7
+ expect(cropPath("/aaa/bbb/ccc/ddd.tsx")).toBe(".../bbb/ccc/ddd.tsx");
8
+ });
9
+
10
+ test("no crop", () => {
11
+ // expect(cropPath("/bbb/ccc/ddd.tsx")).toBe("/bbb/ccc/ddd.tsx");
12
+ expect(cropPath("bbb/ccc/ddd.tsx")).toBe("bbb/ccc/ddd.tsx");
13
+ expect(cropPath("/ccc/ddd.tsx")).toBe("/ccc/ddd.tsx");
14
+ expect(cropPath("ccc/ddd.tsx")).toBe("ccc/ddd.tsx");
15
+ expect(cropPath("")).toBe("");
16
+ expect(cropPath("xxx")).toBe("xxx");
17
+ });
18
+ });
@@ -0,0 +1,12 @@
1
+ export default function cropPath(fullPath: string) {
2
+ const array = fullPath.split("/");
3
+
4
+ const newPath = array
5
+ .slice(Math.max(array.length - 3, 0), array.length)
6
+ .join("/");
7
+ if (newPath !== fullPath) {
8
+ return `.../${newPath}`;
9
+ } else {
10
+ return newPath;
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ import { LabelData } from "../types/LabelData";
2
+ import nonNullable from "./nonNullable";
3
+
4
+ export function deduplicateLabels(labels: LabelData[]): LabelData[] {
5
+ const labelsIds: { [key: string]: true } = {};
6
+ return labels
7
+ .map((label) => {
8
+ const id = JSON.stringify(label);
9
+ if (labelsIds[id]) {
10
+ return null;
11
+ }
12
+ labelsIds[id] = true;
13
+ return label;
14
+ })
15
+ .filter(nonNullable);
16
+ }
@@ -0,0 +1,12 @@
1
+ import { evalTemplate } from "./evalTemplate";
2
+ import { describe, expect, test } from "vitest";
3
+
4
+ describe("evalTemplate", () => {
5
+ test("basic", () => {
6
+ const res = evalTemplate("https://example.com/${filePath}${ext}", {
7
+ filePath: "test",
8
+ ext: ".js",
9
+ });
10
+ expect(res).toBe("https://example.com/test.js");
11
+ });
12
+ });
@@ -0,0 +1,8 @@
1
+ export function evalTemplate(str: string, params: { [key: string]: string }) {
2
+ let newStr = str;
3
+
4
+ Object.entries(params).forEach(([key, value]) => {
5
+ newStr = newStr.replace("${" + key + "}", value);
6
+ });
7
+ return newStr;
8
+ }
@@ -0,0 +1,20 @@
1
+ import { Fiber } from "@locator/shared";
2
+ import { getUsableName } from "./getUsableName";
3
+
4
+ export function findNames(fiber: Fiber): {
5
+ name: string;
6
+ wrappingComponent: string;
7
+ } {
8
+ // if (fiber._debugOwner?.elementType?.styledComponentId) {
9
+ // // This is special case for styled-components, we need to show one level up
10
+ // return {
11
+ // name: getUsableName(fiber._debugOwner),
12
+ // wrappingComponent: getUsableName(fiber._debugOwner?._debugOwner),
13
+ // };
14
+ // } else {
15
+ return {
16
+ name: getUsableName(fiber),
17
+ wrappingComponent: getUsableName(fiber._debugOwner),
18
+ };
19
+ // }
20
+ }
@@ -0,0 +1,80 @@
1
+ import { TreeNode } from "../types/TreeNode";
2
+
3
+ export interface AncestryItem {
4
+ elementName: string;
5
+ componentName?: string;
6
+ filePath?: string;
7
+ line?: number;
8
+ }
9
+
10
+ // Elements to exclude from ancestry (not useful for debugging)
11
+ const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
12
+
13
+ export function collectAncestry(node: TreeNode): AncestryItem[] {
14
+ const items: AncestryItem[] = [];
15
+ let current: TreeNode | null = node;
16
+
17
+ while (current) {
18
+ // Skip html, body, head elements
19
+ if (EXCLUDED_ELEMENTS.has(current.name)) {
20
+ current = current.getParent();
21
+ continue;
22
+ }
23
+
24
+ const component = current.getComponent();
25
+ const source = current.getSource();
26
+
27
+ const item: AncestryItem = {
28
+ elementName: current.name,
29
+ };
30
+
31
+ if (component) {
32
+ item.componentName = component.label;
33
+ if (component.callLink) {
34
+ item.filePath = component.callLink.fileName;
35
+ item.line = component.callLink.lineNumber;
36
+ }
37
+ }
38
+
39
+ if (!item.filePath && source) {
40
+ item.filePath = source.fileName;
41
+ item.line = source.lineNumber;
42
+ }
43
+
44
+ // Only include items that have useful info (component name or file path)
45
+ if (item.componentName || item.filePath) {
46
+ items.push(item);
47
+ }
48
+
49
+ current = current.getParent();
50
+ }
51
+
52
+ return items;
53
+ }
54
+
55
+ export function formatAncestryChain(items: AncestryItem[]): string {
56
+ if (items.length === 0) {
57
+ return "";
58
+ }
59
+
60
+ // Reverse so root is at top, clicked element at bottom
61
+ const reversed = [...items].reverse();
62
+
63
+ const lines: string[] = [];
64
+
65
+ reversed.forEach((item, index) => {
66
+ const indent = " ".repeat(index);
67
+ const prefix = index === 0 ? "" : "└─ ";
68
+
69
+ let description = item.elementName;
70
+ if (item.componentName) {
71
+ description = `${item.elementName} in ${item.componentName}`;
72
+ }
73
+
74
+ const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
75
+
76
+ lines.push(`${indent}${prefix}${description}${location}`);
77
+ });
78
+
79
+ return lines.join("\n");
80
+ }
@@ -0,0 +1,11 @@
1
+ export function getBoundingRect(node: Element | Text): DOMRect | null {
2
+ if (node instanceof Element) {
3
+ return node.getBoundingClientRect();
4
+ } else if (node instanceof Text) {
5
+ const range = document.createRange();
6
+ range.selectNodeContents(node);
7
+ return range.getBoundingClientRect();
8
+ } else {
9
+ return null;
10
+ }
11
+ }
@@ -0,0 +1,25 @@
1
+ import { mergeRects } from "./mergeRects";
2
+ import { SimpleDOMRect, SimpleNode } from "./../types/types";
3
+
4
+ export function getComposedBoundingBox(
5
+ children: SimpleNode[]
6
+ ): SimpleDOMRect | null {
7
+ let composedRect: SimpleDOMRect | null = null;
8
+
9
+ children.forEach((child) => {
10
+ const box = child.box;
11
+ if (!box) {
12
+ return;
13
+ }
14
+ if (box.width <= 0 || box.height <= 0) {
15
+ // ignore zero-sized rects
16
+ return;
17
+ }
18
+ if (composedRect) {
19
+ composedRect = mergeRects(composedRect, box);
20
+ } else {
21
+ composedRect = box;
22
+ }
23
+ });
24
+ return composedRect;
25
+ }
@@ -0,0 +1,21 @@
1
+ import { findFiberByHtmlElement } from "./../adapters/react/findFiberByHtmlElement";
2
+ import { makeFiberId } from "./../adapters/react/makeFiberId";
3
+
4
+ export function getIdsOnPathToRoot(element: HTMLElement): {
5
+ [id: string]: true;
6
+ } {
7
+ const fiber = findFiberByHtmlElement(element, false);
8
+ if (!fiber) {
9
+ return {};
10
+ }
11
+ const res: {
12
+ [id: string]: true;
13
+ } = {};
14
+ let parent = fiber?._debugOwner;
15
+
16
+ while (parent) {
17
+ res[makeFiberId(parent)] = true;
18
+ parent = parent?._debugOwner;
19
+ }
20
+ return res;
21
+ }
@@ -0,0 +1,25 @@
1
+ import { mergeRects } from "./mergeRects";
2
+ import { SimpleDOMRect } from "../types/types";
3
+
4
+ export function getMultipleElementsBoundingBox(
5
+ children: HTMLElement[]
6
+ ): SimpleDOMRect | null {
7
+ let composedRect: SimpleDOMRect | null = null;
8
+
9
+ children.forEach((child) => {
10
+ const box = child.getBoundingClientRect();
11
+ if (!box) {
12
+ return;
13
+ }
14
+ if (box.width <= 0 || box.height <= 0) {
15
+ // ignore zero-sized rects
16
+ return;
17
+ }
18
+ if (composedRect) {
19
+ composedRect = mergeRects(composedRect, box);
20
+ } else {
21
+ composedRect = box;
22
+ }
23
+ });
24
+ return composedRect;
25
+ }
@@ -0,0 +1,17 @@
1
+ import { findFiberByHtmlElement } from "../adapters/react/findFiberByHtmlElement";
2
+ import { fiberToSimple } from "../adapters/react/fiberToSimple";
3
+
4
+ export function getPathToParent(element: HTMLElement) {
5
+ const fiber = findFiberByHtmlElement(element, false);
6
+ if (!fiber) {
7
+ return;
8
+ }
9
+ let res = fiberToSimple(fiber);
10
+ let parent = fiber?._debugOwner;
11
+
12
+ while (parent) {
13
+ res = fiberToSimple(parent, [res]);
14
+ parent = parent?._debugOwner;
15
+ }
16
+ return res;
17
+ }
@@ -0,0 +1,10 @@
1
+ const map = new WeakMap();
2
+ let lastId = 0;
3
+
4
+ export function getReferenceId(ref: object): number {
5
+ if (!map.has(ref)) {
6
+ lastId++;
7
+ map.set(ref, lastId);
8
+ }
9
+ return map.get(ref);
10
+ }