@treelocator/runtime 0.4.5 → 0.4.6

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.
@@ -879,10 +879,6 @@ input:where([type='file']):focus {
879
879
  left: 0.25rem;
880
880
  }
881
881
 
882
- .left-1\\/2 {
883
- left: 50%;
884
- }
885
-
886
882
  .left-3 {
887
883
  left: 0.75rem;
888
884
  }
@@ -895,10 +891,6 @@ input:where([type='file']):focus {
895
891
  top: 0.25rem;
896
892
  }
897
893
 
898
- .top-1\\/2 {
899
- top: 50%;
900
- }
901
-
902
894
  .z-10 {
903
895
  z-index: 10;
904
896
  }
@@ -1078,11 +1070,6 @@ input:where([type='file']):focus {
1078
1070
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1079
1071
  }
1080
1072
 
1081
- .-translate-x-1\\/2 {
1082
- --tw-translate-x: -50%;
1083
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1084
- }
1085
-
1086
1073
  .-translate-x-full {
1087
1074
  --tw-translate-x: -100%;
1088
1075
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1093,11 +1080,6 @@ input:where([type='file']):focus {
1093
1080
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1094
1081
  }
1095
1082
 
1096
- .-translate-y-1\\/2 {
1097
- --tw-translate-y: -50%;
1098
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1099
- }
1100
-
1101
1083
  .translate-x-full {
1102
1084
  --tw-translate-x: 100%;
1103
1085
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1475,11 +1457,6 @@ input:where([type='file']):focus {
1475
1457
  padding-bottom: 0px;
1476
1458
  }
1477
1459
 
1478
- .py-0\\.5 {
1479
- padding-top: 0.125rem;
1480
- padding-bottom: 0.125rem;
1481
- }
1482
-
1483
1460
  .py-1 {
1484
1461
  padding-top: 0.25rem;
1485
1462
  padding-bottom: 0.25rem;
@@ -1813,22 +1790,5 @@ input:where([type='file']):focus {
1813
1790
 
1814
1791
  .ease-out {
1815
1792
  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1816
- }
1817
-
1818
- .hover\\:bg-white\\/30:hover {
1819
- background-color: rgb(255 255 255 / 0.3);
1820
- }
1821
-
1822
- .hover\\:text-gray-100:hover {
1823
- --tw-text-opacity: 1;
1824
- color: rgb(243 244 246 / var(--tw-text-opacity, 1));
1825
- }
1826
-
1827
- .group\\/tooltip:hover .group-hover\\/tooltip\\:visible {
1828
- visibility: visible;
1829
- }
1830
-
1831
- .group\\/tooltip:hover .group-hover\\/tooltip\\:opacity-100 {
1832
- opacity: 1;
1833
1793
  }`;
1834
1794
  export default styles;
@@ -19,8 +19,9 @@ export interface AncestryItem {
19
19
  }
20
20
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
21
21
  /**
22
- * Truncate ancestry from the bottom (clicked element) up to and including
23
- * the first item that has a filePath. Everything above that is discarded.
22
+ * Truncate ancestry to keep only the local context.
23
+ * - If the clicked element has no filePath: keep up to the first ancestor with a file.
24
+ * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file.
24
25
  * The ancestry array is bottom-up: index 0 = clicked element, last = root.
25
26
  */
26
27
  export declare function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[];
@@ -123,14 +123,28 @@ function getInnermostNamedComponent(item) {
123
123
  }
124
124
 
125
125
  /**
126
- * Truncate ancestry from the bottom (clicked element) up to and including
127
- * the first item that has a filePath. Everything above that is discarded.
126
+ * Truncate ancestry to keep only the local context.
127
+ * - If the clicked element has no filePath: keep up to the first ancestor with a file.
128
+ * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file.
128
129
  * The ancestry array is bottom-up: index 0 = clicked element, last = root.
129
130
  */
130
131
  export function truncateAtFirstFile(items) {
131
- const firstWithFile = items.findIndex(item => item.filePath);
132
- if (firstWithFile === -1) return items;
133
- return items.slice(0, firstWithFile + 1);
132
+ if (items.length === 0) return items;
133
+ const clickedFile = items[0]?.filePath;
134
+ if (!clickedFile) {
135
+ // Clicked element has no file: find first ancestor with any file
136
+ const firstWithFile = items.findIndex(item => item.filePath);
137
+ if (firstWithFile === -1) return items;
138
+ return items.slice(0, firstWithFile + 1);
139
+ }
140
+
141
+ // Clicked element has a file: find first ancestor with a different file
142
+ for (let i = 1; i < items.length; i++) {
143
+ if (items[i].filePath && items[i].filePath !== clickedFile) {
144
+ return items.slice(0, i + 1);
145
+ }
146
+ }
147
+ return items;
134
148
  }
135
149
  export function formatAncestryChain(items) {
136
150
  if (items.length === 0) {
@@ -273,7 +273,7 @@ describe("formatAncestryChain", () => {
273
273
  line: 10
274
274
  }]);
275
275
  });
276
- it("returns just the clicked element when it already has a filePath", () => {
276
+ it("when clicked element has filePath, keeps up to first different file", () => {
277
277
  const items = [{
278
278
  elementName: "button",
279
279
  componentName: "Button",
@@ -296,8 +296,66 @@ describe("formatAncestryChain", () => {
296
296
  componentName: "Button",
297
297
  filePath: "src/Button.tsx",
298
298
  line: 5
299
+ }, {
300
+ elementName: "div",
301
+ componentName: "Layout",
302
+ filePath: "src/Layout.tsx",
303
+ line: 10
304
+ }]);
305
+ });
306
+ it("keeps all items in the same file plus first different-file ancestor", () => {
307
+ const items = [{
308
+ elementName: "span",
309
+ componentName: "Label",
310
+ filePath: "src/Button.tsx",
311
+ line: 20
312
+ }, {
313
+ elementName: "button",
314
+ componentName: "Button",
315
+ filePath: "src/Button.tsx",
316
+ line: 5
317
+ }, {
318
+ elementName: "div",
319
+ componentName: "Layout",
320
+ filePath: "src/Layout.tsx",
321
+ line: 10
322
+ }, {
323
+ elementName: "div",
324
+ componentName: "App",
325
+ filePath: "src/App.tsx",
326
+ line: 1
327
+ }];
328
+ const result = truncateAtFirstFile(items);
329
+ expect(result).toEqual([{
330
+ elementName: "span",
331
+ componentName: "Label",
332
+ filePath: "src/Button.tsx",
333
+ line: 20
334
+ }, {
335
+ elementName: "button",
336
+ componentName: "Button",
337
+ filePath: "src/Button.tsx",
338
+ line: 5
339
+ }, {
340
+ elementName: "div",
341
+ componentName: "Layout",
342
+ filePath: "src/Layout.tsx",
343
+ line: 10
299
344
  }]);
300
345
  });
346
+ it("returns all items when all share the same file", () => {
347
+ const items = [{
348
+ elementName: "span",
349
+ filePath: "src/App.tsx",
350
+ line: 10
351
+ }, {
352
+ elementName: "div",
353
+ filePath: "src/App.tsx",
354
+ line: 5
355
+ }];
356
+ const result = truncateAtFirstFile(items);
357
+ expect(result).toEqual(items);
358
+ });
301
359
  it("returns all items when none have a filePath", () => {
302
360
  const items = [{
303
361
  elementName: "span",
@@ -9,8 +9,12 @@ export function getMouseModifiers() {
9
9
  }
10
10
  export function isCombinationModifiersPressed(e, rightClick = false) {
11
11
  const modifiers = getMouseModifiers();
12
+
13
+ // Only require shift if it's part of the configured modifiers.
14
+ // Shift is used independently for truncation, so pressing it shouldn't
15
+ // disqualify the activation modifier combo.
12
16
  if (rightClick) {
13
- return e.altKey == !!modifiers.alt && e.metaKey == !!modifiers.meta && e.shiftKey == !!modifiers.shift;
17
+ return e.altKey == !!modifiers.alt && e.metaKey == !!modifiers.meta && (!modifiers.shift || e.shiftKey);
14
18
  }
15
- return e.altKey == !!modifiers.alt && e.ctrlKey == !!modifiers.ctrl && e.metaKey == !!modifiers.meta && e.shiftKey == !!modifiers.shift;
19
+ return e.altKey == !!modifiers.alt && e.ctrlKey == !!modifiers.ctrl && e.metaKey == !!modifiers.meta && (!modifiers.shift || e.shiftKey);
16
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treelocator/runtime",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
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",
@@ -879,10 +879,6 @@ input:where([type='file']):focus {
879
879
  left: 0.25rem;
880
880
  }
881
881
 
882
- .left-1\\/2 {
883
- left: 50%;
884
- }
885
-
886
882
  .left-3 {
887
883
  left: 0.75rem;
888
884
  }
@@ -895,10 +891,6 @@ input:where([type='file']):focus {
895
891
  top: 0.25rem;
896
892
  }
897
893
 
898
- .top-1\\/2 {
899
- top: 50%;
900
- }
901
-
902
894
  .z-10 {
903
895
  z-index: 10;
904
896
  }
@@ -1078,11 +1070,6 @@ input:where([type='file']):focus {
1078
1070
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1079
1071
  }
1080
1072
 
1081
- .-translate-x-1\\/2 {
1082
- --tw-translate-x: -50%;
1083
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1084
- }
1085
-
1086
1073
  .-translate-x-full {
1087
1074
  --tw-translate-x: -100%;
1088
1075
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1093,11 +1080,6 @@ input:where([type='file']):focus {
1093
1080
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1094
1081
  }
1095
1082
 
1096
- .-translate-y-1\\/2 {
1097
- --tw-translate-y: -50%;
1098
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1099
- }
1100
-
1101
1083
  .translate-x-full {
1102
1084
  --tw-translate-x: 100%;
1103
1085
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1475,11 +1457,6 @@ input:where([type='file']):focus {
1475
1457
  padding-bottom: 0px;
1476
1458
  }
1477
1459
 
1478
- .py-0\\.5 {
1479
- padding-top: 0.125rem;
1480
- padding-bottom: 0.125rem;
1481
- }
1482
-
1483
1460
  .py-1 {
1484
1461
  padding-top: 0.25rem;
1485
1462
  padding-bottom: 0.25rem;
@@ -1813,22 +1790,5 @@ input:where([type='file']):focus {
1813
1790
 
1814
1791
  .ease-out {
1815
1792
  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1816
- }
1817
-
1818
- .hover\\:bg-white\\/30:hover {
1819
- background-color: rgb(255 255 255 / 0.3);
1820
- }
1821
-
1822
- .hover\\:text-gray-100:hover {
1823
- --tw-text-opacity: 1;
1824
- color: rgb(243 244 246 / var(--tw-text-opacity, 1));
1825
- }
1826
-
1827
- .group\\/tooltip:hover .group-hover\\/tooltip\\:visible {
1828
- visibility: visible;
1829
- }
1830
-
1831
- .group\\/tooltip:hover .group-hover\\/tooltip\\:opacity-100 {
1832
- opacity: 1;
1833
1793
  }`;
1834
1794
  export default styles;
@@ -286,7 +286,7 @@ describe("formatAncestryChain", () => {
286
286
  ]);
287
287
  });
288
288
 
289
- it("returns just the clicked element when it already has a filePath", () => {
289
+ it("when clicked element has filePath, keeps up to first different file", () => {
290
290
  const items: AncestryItem[] = [
291
291
  { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
292
292
  { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
@@ -296,9 +296,36 @@ describe("formatAncestryChain", () => {
296
296
  const result = truncateAtFirstFile(items);
297
297
  expect(result).toEqual([
298
298
  { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
299
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
299
300
  ]);
300
301
  });
301
302
 
303
+ it("keeps all items in the same file plus first different-file ancestor", () => {
304
+ const items: AncestryItem[] = [
305
+ { elementName: "span", componentName: "Label", filePath: "src/Button.tsx", line: 20 },
306
+ { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
307
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
308
+ { elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
309
+ ];
310
+
311
+ const result = truncateAtFirstFile(items);
312
+ expect(result).toEqual([
313
+ { elementName: "span", componentName: "Label", filePath: "src/Button.tsx", line: 20 },
314
+ { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
315
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
316
+ ]);
317
+ });
318
+
319
+ it("returns all items when all share the same file", () => {
320
+ const items: AncestryItem[] = [
321
+ { elementName: "span", filePath: "src/App.tsx", line: 10 },
322
+ { elementName: "div", filePath: "src/App.tsx", line: 5 },
323
+ ];
324
+
325
+ const result = truncateAtFirstFile(items);
326
+ expect(result).toEqual(items);
327
+ });
328
+
302
329
  it("returns all items when none have a filePath", () => {
303
330
  const items: AncestryItem[] = [
304
331
  { elementName: "span", componentName: "A" },
@@ -165,14 +165,31 @@ function getInnermostNamedComponent(item: AncestryItem | null | undefined): stri
165
165
  }
166
166
 
167
167
  /**
168
- * Truncate ancestry from the bottom (clicked element) up to and including
169
- * the first item that has a filePath. Everything above that is discarded.
168
+ * Truncate ancestry to keep only the local context.
169
+ * - If the clicked element has no filePath: keep up to the first ancestor with a file.
170
+ * - If the clicked element has a filePath: keep up to the first ancestor with a DIFFERENT file.
170
171
  * The ancestry array is bottom-up: index 0 = clicked element, last = root.
171
172
  */
172
173
  export function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[] {
173
- const firstWithFile = items.findIndex((item) => item.filePath);
174
- if (firstWithFile === -1) return items;
175
- return items.slice(0, firstWithFile + 1);
174
+ if (items.length === 0) return items;
175
+
176
+ const clickedFile = items[0]?.filePath;
177
+
178
+ if (!clickedFile) {
179
+ // Clicked element has no file: find first ancestor with any file
180
+ const firstWithFile = items.findIndex((item) => item.filePath);
181
+ if (firstWithFile === -1) return items;
182
+ return items.slice(0, firstWithFile + 1);
183
+ }
184
+
185
+ // Clicked element has a file: find first ancestor with a different file
186
+ for (let i = 1; i < items.length; i++) {
187
+ if (items[i]!.filePath && items[i]!.filePath !== clickedFile) {
188
+ return items.slice(0, i + 1);
189
+ }
190
+ }
191
+
192
+ return items;
176
193
  }
177
194
 
178
195
  export function formatAncestryChain(items: AncestryItem[]): string {
@@ -16,17 +16,20 @@ export function isCombinationModifiersPressed(
16
16
  ) {
17
17
  const modifiers = getMouseModifiers();
18
18
 
19
+ // Only require shift if it's part of the configured modifiers.
20
+ // Shift is used independently for truncation, so pressing it shouldn't
21
+ // disqualify the activation modifier combo.
19
22
  if (rightClick) {
20
23
  return (
21
24
  e.altKey == !!modifiers.alt &&
22
25
  e.metaKey == !!modifiers.meta &&
23
- e.shiftKey == !!modifiers.shift
26
+ (!modifiers.shift || e.shiftKey)
24
27
  );
25
28
  }
26
29
  return (
27
30
  e.altKey == !!modifiers.alt &&
28
31
  e.ctrlKey == !!modifiers.ctrl &&
29
32
  e.metaKey == !!modifiers.meta &&
30
- e.shiftKey == !!modifiers.shift
33
+ (!modifiers.shift || e.shiftKey)
31
34
  );
32
35
  }