@treelocator/runtime 0.4.5 → 0.4.7
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/dist/_generated_styles.js +0 -40
- package/dist/functions/formatAncestryChain.d.ts +4 -2
- package/dist/functions/formatAncestryChain.js +35 -5
- package/dist/functions/formatAncestryChain.test.js +117 -1
- package/dist/functions/isCombinationModifiersPressed.js +6 -2
- package/package.json +1 -1
- package/src/_generated_styles.ts +0 -40
- package/src/functions/formatAncestryChain.test.ts +56 -1
- package/src/functions/formatAncestryChain.ts +38 -5
- package/src/functions/isCombinationModifiersPressed.ts +5 -2
|
@@ -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,10 @@ export interface AncestryItem {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function collectAncestry(node: TreeNode): AncestryItem[];
|
|
21
21
|
/**
|
|
22
|
-
* Truncate ancestry
|
|
23
|
-
* the
|
|
22
|
+
* Truncate ancestry to keep only the local context.
|
|
23
|
+
* - If the clicked element has no file: keep up to the first ancestor with a file.
|
|
24
|
+
* - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file.
|
|
25
|
+
* Checks both client filePath and serverComponents for file info.
|
|
24
26
|
* The ancestry array is bottom-up: index 0 = clicked element, last = root.
|
|
25
27
|
*/
|
|
26
28
|
export declare function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[];
|
|
@@ -123,14 +123,44 @@ function getInnermostNamedComponent(item) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
126
|
+
* Get the effective file path from an AncestryItem, checking both
|
|
127
|
+
* client filePath and serverComponents (Next.js RSC, Phoenix, etc.).
|
|
128
|
+
*/
|
|
129
|
+
function getItemFilePath(item) {
|
|
130
|
+
if (item.filePath) return item.filePath;
|
|
131
|
+
if (item.serverComponents && item.serverComponents.length > 0) {
|
|
132
|
+
const comp = item.serverComponents.find(sc => sc.type === "component");
|
|
133
|
+
if (comp) return comp.filePath;
|
|
134
|
+
return item.serverComponents[0].filePath;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Truncate ancestry to keep only the local context.
|
|
141
|
+
* - If the clicked element has no file: keep up to the first ancestor with a file.
|
|
142
|
+
* - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file.
|
|
143
|
+
* Checks both client filePath and serverComponents for file info.
|
|
128
144
|
* The ancestry array is bottom-up: index 0 = clicked element, last = root.
|
|
129
145
|
*/
|
|
130
146
|
export function truncateAtFirstFile(items) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
if (items.length === 0) return items;
|
|
148
|
+
const clickedFile = getItemFilePath(items[0]);
|
|
149
|
+
if (!clickedFile) {
|
|
150
|
+
// Clicked element has no file: find first ancestor with any file
|
|
151
|
+
const firstWithFile = items.findIndex(item => getItemFilePath(item));
|
|
152
|
+
if (firstWithFile === -1) return items;
|
|
153
|
+
return items.slice(0, firstWithFile + 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clicked element has a file: find first ancestor with a different file
|
|
157
|
+
for (let i = 1; i < items.length; i++) {
|
|
158
|
+
const ancestorFile = getItemFilePath(items[i]);
|
|
159
|
+
if (ancestorFile && ancestorFile !== clickedFile) {
|
|
160
|
+
return items.slice(0, i + 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return items;
|
|
134
164
|
}
|
|
135
165
|
export function formatAncestryChain(items) {
|
|
136
166
|
if (items.length === 0) {
|
|
@@ -273,7 +273,7 @@ describe("formatAncestryChain", () => {
|
|
|
273
273
|
line: 10
|
|
274
274
|
}]);
|
|
275
275
|
});
|
|
276
|
-
it("
|
|
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",
|
|
@@ -309,6 +367,64 @@ describe("formatAncestryChain", () => {
|
|
|
309
367
|
const result = truncateAtFirstFile(items);
|
|
310
368
|
expect(result).toEqual(items);
|
|
311
369
|
});
|
|
370
|
+
it("uses serverComponents file path when filePath is missing", () => {
|
|
371
|
+
const items = [{
|
|
372
|
+
elementName: "div",
|
|
373
|
+
componentName: "TurnActivityBox",
|
|
374
|
+
serverComponents: [{
|
|
375
|
+
name: "TurnActivityBox",
|
|
376
|
+
filePath: "components/MessageRow.tsx",
|
|
377
|
+
line: 921,
|
|
378
|
+
type: "component"
|
|
379
|
+
}]
|
|
380
|
+
}, {
|
|
381
|
+
elementName: "div",
|
|
382
|
+
componentName: "MessageRow",
|
|
383
|
+
serverComponents: [{
|
|
384
|
+
name: "MessageRow",
|
|
385
|
+
filePath: "components/chat/ChatViewport.tsx",
|
|
386
|
+
line: 917,
|
|
387
|
+
type: "component"
|
|
388
|
+
}]
|
|
389
|
+
}, {
|
|
390
|
+
elementName: "main",
|
|
391
|
+
componentName: "Home",
|
|
392
|
+
serverComponents: [{
|
|
393
|
+
name: "Home",
|
|
394
|
+
filePath: "app/page.tsx",
|
|
395
|
+
line: 817,
|
|
396
|
+
type: "component"
|
|
397
|
+
}]
|
|
398
|
+
}];
|
|
399
|
+
const result = truncateAtFirstFile(items);
|
|
400
|
+
expect(result).toEqual([items[0], items[1]]);
|
|
401
|
+
});
|
|
402
|
+
it("uses serverComponents when clicked element has no filePath but has serverComponents", () => {
|
|
403
|
+
const items = [{
|
|
404
|
+
elementName: "span",
|
|
405
|
+
componentName: "Button"
|
|
406
|
+
}, {
|
|
407
|
+
elementName: "div",
|
|
408
|
+
componentName: "Card",
|
|
409
|
+
serverComponents: [{
|
|
410
|
+
name: "Card",
|
|
411
|
+
filePath: "src/Card.tsx",
|
|
412
|
+
line: 10,
|
|
413
|
+
type: "component"
|
|
414
|
+
}]
|
|
415
|
+
}, {
|
|
416
|
+
elementName: "div",
|
|
417
|
+
componentName: "App",
|
|
418
|
+
serverComponents: [{
|
|
419
|
+
name: "App",
|
|
420
|
+
filePath: "src/App.tsx",
|
|
421
|
+
line: 1,
|
|
422
|
+
type: "component"
|
|
423
|
+
}]
|
|
424
|
+
}];
|
|
425
|
+
const result = truncateAtFirstFile(items);
|
|
426
|
+
expect(result).toEqual([items[0], items[1]]);
|
|
427
|
+
});
|
|
312
428
|
it("returns empty array for empty input", () => {
|
|
313
429
|
expect(truncateAtFirstFile([])).toEqual([]);
|
|
314
430
|
});
|
|
@@ -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 &&
|
|
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 &&
|
|
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.
|
|
3
|
+
"version": "0.4.7",
|
|
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",
|
package/src/_generated_styles.ts
CHANGED
|
@@ -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("
|
|
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" },
|
|
@@ -309,6 +336,34 @@ describe("formatAncestryChain", () => {
|
|
|
309
336
|
expect(result).toEqual(items);
|
|
310
337
|
});
|
|
311
338
|
|
|
339
|
+
it("uses serverComponents file path when filePath is missing", () => {
|
|
340
|
+
const items: AncestryItem[] = [
|
|
341
|
+
{ elementName: "div", componentName: "TurnActivityBox", serverComponents: [{ name: "TurnActivityBox", filePath: "components/MessageRow.tsx", line: 921, type: "component" }] },
|
|
342
|
+
{ elementName: "div", componentName: "MessageRow", serverComponents: [{ name: "MessageRow", filePath: "components/chat/ChatViewport.tsx", line: 917, type: "component" }] },
|
|
343
|
+
{ elementName: "main", componentName: "Home", serverComponents: [{ name: "Home", filePath: "app/page.tsx", line: 817, type: "component" }] },
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const result = truncateAtFirstFile(items);
|
|
347
|
+
expect(result).toEqual([
|
|
348
|
+
items[0],
|
|
349
|
+
items[1],
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("uses serverComponents when clicked element has no filePath but has serverComponents", () => {
|
|
354
|
+
const items: AncestryItem[] = [
|
|
355
|
+
{ elementName: "span", componentName: "Button" },
|
|
356
|
+
{ elementName: "div", componentName: "Card", serverComponents: [{ name: "Card", filePath: "src/Card.tsx", line: 10, type: "component" }] },
|
|
357
|
+
{ elementName: "div", componentName: "App", serverComponents: [{ name: "App", filePath: "src/App.tsx", line: 1, type: "component" }] },
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
const result = truncateAtFirstFile(items);
|
|
361
|
+
expect(result).toEqual([
|
|
362
|
+
items[0],
|
|
363
|
+
items[1],
|
|
364
|
+
]);
|
|
365
|
+
});
|
|
366
|
+
|
|
312
367
|
it("returns empty array for empty input", () => {
|
|
313
368
|
expect(truncateAtFirstFile([])).toEqual([]);
|
|
314
369
|
});
|
|
@@ -165,14 +165,47 @@ function getInnermostNamedComponent(item: AncestryItem | null | undefined): stri
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
168
|
+
* Get the effective file path from an AncestryItem, checking both
|
|
169
|
+
* client filePath and serverComponents (Next.js RSC, Phoenix, etc.).
|
|
170
|
+
*/
|
|
171
|
+
function getItemFilePath(item: AncestryItem): string | undefined {
|
|
172
|
+
if (item.filePath) return item.filePath;
|
|
173
|
+
if (item.serverComponents && item.serverComponents.length > 0) {
|
|
174
|
+
const comp = item.serverComponents.find((sc) => sc.type === "component");
|
|
175
|
+
if (comp) return comp.filePath;
|
|
176
|
+
return item.serverComponents[0]!.filePath;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Truncate ancestry to keep only the local context.
|
|
183
|
+
* - If the clicked element has no file: keep up to the first ancestor with a file.
|
|
184
|
+
* - If the clicked element has a file: keep up to the first ancestor with a DIFFERENT file.
|
|
185
|
+
* Checks both client filePath and serverComponents for file info.
|
|
170
186
|
* The ancestry array is bottom-up: index 0 = clicked element, last = root.
|
|
171
187
|
*/
|
|
172
188
|
export function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[] {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
if (items.length === 0) return items;
|
|
190
|
+
|
|
191
|
+
const clickedFile = getItemFilePath(items[0]!);
|
|
192
|
+
|
|
193
|
+
if (!clickedFile) {
|
|
194
|
+
// Clicked element has no file: find first ancestor with any file
|
|
195
|
+
const firstWithFile = items.findIndex((item) => getItemFilePath(item));
|
|
196
|
+
if (firstWithFile === -1) return items;
|
|
197
|
+
return items.slice(0, firstWithFile + 1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Clicked element has a file: find first ancestor with a different file
|
|
201
|
+
for (let i = 1; i < items.length; i++) {
|
|
202
|
+
const ancestorFile = getItemFilePath(items[i]!);
|
|
203
|
+
if (ancestorFile && ancestorFile !== clickedFile) {
|
|
204
|
+
return items.slice(0, i + 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return items;
|
|
176
209
|
}
|
|
177
210
|
|
|
178
211
|
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
|
-
|
|
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
|
-
|
|
33
|
+
(!modifiers.shift || e.shiftKey)
|
|
31
34
|
);
|
|
32
35
|
}
|