@treelocator/runtime 0.4.0 → 0.4.5

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 (67) hide show
  1. package/dist/_generated_styles.js +115 -15
  2. package/dist/adapters/getElementInfo.js +3 -17
  3. package/dist/adapters/getParentsPath.js +3 -33
  4. package/dist/adapters/getTree.js +3 -33
  5. package/dist/adapters/jsx/getJSXComponentBoundingBox.js +0 -1
  6. package/dist/adapters/jsx/jsxAdapter.js +0 -3
  7. package/dist/adapters/jsx/runtimeStore.js +0 -12
  8. package/dist/adapters/react/reactAdapter.js +0 -8
  9. package/dist/adapters/resolveAdapter.d.ts +8 -0
  10. package/dist/adapters/resolveAdapter.js +28 -0
  11. package/dist/adapters/vue/vueAdapter.js +0 -14
  12. package/dist/browserApi.d.ts +1 -1
  13. package/dist/browserApi.js +8 -10
  14. package/dist/components/MaybeOutline.d.ts +1 -0
  15. package/dist/components/MaybeOutline.js +38 -29
  16. package/dist/components/Outline.d.ts +1 -0
  17. package/dist/components/Outline.js +20 -16
  18. package/dist/components/Runtime.js +30 -18
  19. package/dist/dejitter/recorder.js +8 -13
  20. package/dist/functions/formatAncestryChain.d.ts +6 -0
  21. package/dist/functions/formatAncestryChain.js +11 -0
  22. package/dist/functions/formatAncestryChain.test.js +75 -1
  23. package/dist/functions/getUsableName.js +0 -21
  24. package/dist/output.css +10 -40
  25. package/dist/types/types.d.ts +1 -32
  26. package/package.json +1 -1
  27. package/src/_generated_styles.ts +115 -15
  28. package/src/adapters/getElementInfo.tsx +3 -23
  29. package/src/adapters/getParentsPath.tsx +3 -42
  30. package/src/adapters/getTree.tsx +3 -42
  31. package/src/adapters/jsx/getJSXComponentBoundingBox.ts +0 -1
  32. package/src/adapters/jsx/jsxAdapter.ts +0 -2
  33. package/src/adapters/jsx/runtimeStore.ts +0 -11
  34. package/src/adapters/react/reactAdapter.ts +0 -7
  35. package/src/adapters/resolveAdapter.ts +38 -0
  36. package/src/adapters/vue/vueAdapter.ts +0 -14
  37. package/src/browserApi.ts +9 -12
  38. package/src/components/MaybeOutline.tsx +4 -2
  39. package/src/components/Outline.tsx +2 -1
  40. package/src/components/Runtime.tsx +27 -18
  41. package/src/dejitter/recorder.ts +51 -56
  42. package/src/functions/formatAncestryChain.test.ts +47 -0
  43. package/src/functions/formatAncestryChain.ts +11 -0
  44. package/src/functions/getUsableName.ts +0 -21
  45. package/src/types/types.ts +1 -32
  46. package/src/adapters/react/fiberToSimple.tsx +0 -72
  47. package/src/adapters/react/gatherFiberRoots.tsx +0 -36
  48. package/src/adapters/react/makeFiberId.tsx +0 -19
  49. package/src/adapters/react/searchDevtoolsRenderersForClosestTarget.ts +0 -15
  50. package/src/components/Button.tsx +0 -14
  51. package/src/components/ComponentOutline.tsx +0 -98
  52. package/src/components/SimpleNodeOutline.tsx +0 -27
  53. package/src/components/Tooltip.tsx +0 -28
  54. package/src/functions/cropPath.test.ts +0 -18
  55. package/src/functions/cropPath.ts +0 -12
  56. package/src/functions/evalTemplate.test.ts +0 -12
  57. package/src/functions/evalTemplate.ts +0 -8
  58. package/src/functions/findNames.ts +0 -20
  59. package/src/functions/getBoundingRect.tsx +0 -11
  60. package/src/functions/getComposedBoundingBox.tsx +0 -25
  61. package/src/functions/getIdsOnPathToRoot.tsx +0 -21
  62. package/src/functions/getMultipleElementsBoundingBox.tsx +0 -25
  63. package/src/functions/getPathToParent.tsx +0 -17
  64. package/src/functions/getUsableFileName.test.tsx +0 -24
  65. package/src/functions/getUsableFileName.tsx +0 -19
  66. package/src/functions/transformPath.test.ts +0 -28
  67. package/src/functions/transformPath.ts +0 -7
@@ -7,7 +7,7 @@ import { Targets as SetupTargets } from "../types/types";
7
7
  import { MaybeOutline } from "./MaybeOutline";
8
8
  import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
9
9
  import { Toast } from "./Toast";
10
- import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
10
+ import { collectAncestry, formatAncestryChain, truncateAtFirstFile } from "../functions/formatAncestryChain";
11
11
  import { enrichAncestryWithSourceMaps } from "../functions/enrichAncestrySourceMaps";
12
12
  import { createTreeNode } from "../adapters/createTreeNode";
13
13
  import treeIconUrl from "../_generated_tree_icon";
@@ -15,6 +15,15 @@ import { createDejitterRecorder, DejitterAPI, DejitterFinding, DejitterSummary }
15
15
  import { RecordingOutline } from "./RecordingOutline";
16
16
  import { RecordingResults, InteractionEvent } from "./RecordingResults";
17
17
 
18
+ const DEJITTER_CONFIG: Partial<import("../dejitter/recorder").DejitterConfig> = {
19
+ selector: '[data-treelocator-recording]',
20
+ props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
21
+ sampleRate: 15,
22
+ maxDuration: 30000,
23
+ idleTimeout: 0,
24
+ mutations: true,
25
+ };
26
+
18
27
  type RuntimeProps = {
19
28
  adapterId?: AdapterId;
20
29
  targets: Targets;
@@ -22,6 +31,7 @@ type RuntimeProps = {
22
31
 
23
32
  function Runtime(props: RuntimeProps) {
24
33
  const [holdingModKey, setHoldingModKey] = createSignal<boolean>(false);
34
+ const [holdingShift, setHoldingShift] = createSignal<boolean>(false);
25
35
  const [currentElement, setCurrentElement] = createSignal<HTMLElement | null>(
26
36
  null
27
37
  );
@@ -98,15 +108,18 @@ function Runtime(props: RuntimeProps) {
98
108
 
99
109
  function keyUpListener(e: KeyboardEvent) {
100
110
  setHoldingModKey(isCombinationModifiersPressed(e));
111
+ setHoldingShift(e.shiftKey);
101
112
  }
102
113
 
103
114
  function keyDownListener(e: KeyboardEvent) {
104
115
  setHoldingModKey(isCombinationModifiersPressed(e, true));
116
+ setHoldingShift(e.shiftKey);
105
117
  }
106
118
 
107
119
  function mouseMoveListener(e: MouseEvent) {
108
120
  // Update modifier state from mouse events - more reliable than keydown/keyup
109
121
  setHoldingModKey(e.altKey);
122
+ setHoldingShift(e.shiftKey);
110
123
  }
111
124
 
112
125
  function findElementAtPoint(e: MouseEvent): HTMLElement | null {
@@ -155,14 +168,7 @@ function Runtime(props: RuntimeProps) {
155
168
  setRecordedElement(element);
156
169
 
157
170
  dejitterInstance = createDejitterRecorder();
158
- dejitterInstance.configure({
159
- selector: '[data-treelocator-recording]',
160
- props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
161
- sampleRate: 15,
162
- maxDuration: 30000,
163
- idleTimeout: 0,
164
- mutations: true,
165
- });
171
+ dejitterInstance.configure(DEJITTER_CONFIG);
166
172
  dejitterInstance.start();
167
173
  startInteractionTracker();
168
174
  setRecordingState('recording');
@@ -288,14 +294,7 @@ function Runtime(props: RuntimeProps) {
288
294
  setRecordedElement(element);
289
295
 
290
296
  dejitterInstance = createDejitterRecorder();
291
- dejitterInstance.configure({
292
- selector: '[data-treelocator-recording]',
293
- props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
294
- sampleRate: 15,
295
- maxDuration: 30000,
296
- idleTimeout: 0,
297
- mutations: true,
298
- });
297
+ dejitterInstance.configure(DEJITTER_CONFIG);
299
298
  dejitterInstance.start();
300
299
  setRecordingState('recording');
301
300
  setReplaying(true);
@@ -382,6 +381,7 @@ function Runtime(props: RuntimeProps) {
382
381
  setRecordedElement(null);
383
382
  setViewingPrevious(false);
384
383
  setRecordingState('idle');
384
+ try { localStorage.removeItem(STORAGE_KEY); } catch {}
385
385
  }
386
386
 
387
387
  function hasPreviousRecording(): boolean {
@@ -443,6 +443,7 @@ function Runtime(props: RuntimeProps) {
443
443
 
444
444
  function mouseOverListener(e: MouseEvent) {
445
445
  setHoldingModKey(e.altKey);
446
+ setHoldingShift(e.shiftKey);
446
447
 
447
448
  // Don't update hovered element while recording -- highlight is sticky
448
449
  if (recordingState() === 'recording') return;
@@ -501,7 +502,12 @@ function Runtime(props: RuntimeProps) {
501
502
  // Copy ancestry to clipboard on alt+click
502
503
  const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
503
504
  if (treeNode) {
504
- const ancestry = collectAncestry(treeNode);
505
+ let ancestry = collectAncestry(treeNode);
506
+
507
+ // Alt+Shift: keep from bottom up to the first element with a file, discard above
508
+ if (e.shiftKey) {
509
+ ancestry = truncateAtFirstFile(ancestry);
510
+ }
505
511
 
506
512
  // Write immediately with component names (preserves user gesture for clipboard API)
507
513
  const formatted = formatAncestryChain(ancestry);
@@ -564,6 +570,8 @@ function Runtime(props: RuntimeProps) {
564
570
  }
565
571
 
566
572
  onCleanup(() => {
573
+ stopReplay();
574
+ stopInteractionTracker();
567
575
  for (const root of roots) {
568
576
  root.removeEventListener("keyup", keyUpListener as EventListener);
569
577
  root.removeEventListener("keydown", keyDownListener as EventListener);
@@ -596,6 +604,7 @@ function Runtime(props: RuntimeProps) {
596
604
  currentElement={currentElement()!}
597
605
  adapterId={props.adapterId}
598
606
  targets={props.targets}
607
+ dashed={holdingShift()}
599
608
  />
600
609
  ) : null}
601
610
  {recordingState() === 'recording' && recordedElement() ? (
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Dejitter — Animation frame recorder & jank detector (vendored)
4
3
  *
@@ -218,13 +217,13 @@ export function createDejitterRecorder(): DejitterAPI {
218
217
  function downsample(): any[] {
219
218
  if (rawFrames.length === 0) return [];
220
219
 
221
- const duration = rawFrames[rawFrames.length - 1].t;
220
+ const duration = rawFrames[rawFrames.length - 1]!.t;
222
221
  const targetFrames = Math.max(1, Math.round((duration / 1000) * config.sampleRate));
223
222
 
224
223
  const changeIndex = new Map<string, Array<{ frameIdx: number; t: number; value: any }>>();
225
224
 
226
225
  for (let fi = 0; fi < rawFrames.length; fi++) {
227
- const frame = rawFrames[fi];
226
+ const frame = rawFrames[fi]!;
228
227
  for (const change of frame.changes) {
229
228
  const { id, ...props } = change;
230
229
  for (const [prop, value] of Object.entries(props)) {
@@ -251,7 +250,7 @@ export function createDejitterRecorder(): DejitterAPI {
251
250
  } else {
252
251
  for (let i = 0; i < targetFrames; i++) {
253
252
  const srcIdx = Math.round((i / (targetFrames - 1)) * (changes.length - 1));
254
- const c = changes[srcIdx];
253
+ const c = changes[srcIdx]!;
255
254
  outputEvents.push({ t: c.t, id, prop, value: c.value });
256
255
  }
257
256
  }
@@ -296,7 +295,7 @@ export function createDejitterRecorder(): DejitterAPI {
296
295
 
297
296
  function buildPropStats(): { targetFrames: number; props: any[] } {
298
297
  const stats: Record<string, any> = {};
299
- const duration = rawFrames.length ? rawFrames[rawFrames.length - 1].t : 0;
298
+ const duration = rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0;
300
299
  const targetFrames = Math.max(1, Math.round((duration / 1000) * config.sampleRate));
301
300
 
302
301
  for (const f of rawFrames) {
@@ -344,7 +343,7 @@ export function createDejitterRecorder(): DejitterAPI {
344
343
  if (value === 'none' || value === '' || value === 'auto') return 0;
345
344
  const matrixMatch = String(value).match(/^matrix\(([^)]+)\)$/);
346
345
  if (matrixMatch) {
347
- const parts = matrixMatch[1].split(',').map(Number);
346
+ const parts = matrixMatch[1]!.split(',').map(Number);
348
347
  const tx = parts[4] || 0;
349
348
  const ty = parts[5] || 0;
350
349
  return Math.abs(tx) > Math.abs(ty) ? tx : ty;
@@ -356,8 +355,8 @@ export function createDejitterRecorder(): DejitterAPI {
356
355
  function detectBounce(timeline: Array<{ t: number; value: any }>): any | null {
357
356
  if (timeline.length < 3) return null;
358
357
 
359
- const first = timeline[0].value;
360
- const last = timeline[timeline.length - 1].value;
358
+ const first = timeline[0]!.value;
359
+ const last = timeline[timeline.length - 1]!.value;
361
360
  if (first !== last) return null;
362
361
 
363
362
  const firstNum = extractNumeric(first);
@@ -384,9 +383,9 @@ export function createDejitterRecorder(): DejitterAPI {
384
383
  peak: peakValue,
385
384
  peakDeviation: Math.round(peakDeviation * 10) / 10,
386
385
  peakT,
387
- startT: timeline[0].t,
388
- endT: timeline[timeline.length - 1].t,
389
- duration: timeline[timeline.length - 1].t - timeline[0].t,
386
+ startT: timeline[0]!.t,
387
+ endT: timeline[timeline.length - 1]!.t,
388
+ duration: timeline[timeline.length - 1]!.t - timeline[0]!.t,
390
389
  };
391
390
  }
392
391
 
@@ -394,7 +393,7 @@ export function createDejitterRecorder(): DejitterAPI {
394
393
  const byElem: Record<string, any[]> = {};
395
394
  for (const p of propStats.props) {
396
395
  if (!byElem[p.elem]) byElem[p.elem] = [];
397
- byElem[p.elem].push(p);
396
+ byElem[p.elem]!.push(p);
398
397
  }
399
398
 
400
399
  const outliers: any[] = [];
@@ -427,8 +426,8 @@ export function createDejitterRecorder(): DejitterAPI {
427
426
  function countReversals(numeric: Array<{ t: number; val: number }>): number {
428
427
  let reversals = 0;
429
428
  for (let i = 2; i < numeric.length; i++) {
430
- const d1 = numeric[i - 1].val - numeric[i - 2].val;
431
- const d2 = numeric[i].val - numeric[i - 1].val;
429
+ const d1 = numeric[i - 1]!.val - numeric[i - 2]!.val;
430
+ const d2 = numeric[i]!.val - numeric[i - 1]!.val;
432
431
  if (Math.abs(d1) > config.thresholds.shiver.minDelta && Math.abs(d2) > config.thresholds.shiver.minDelta && d1 * d2 < 0) {
433
432
  reversals++;
434
433
  }
@@ -485,12 +484,11 @@ export function createDejitterRecorder(): DejitterAPI {
485
484
  return findings;
486
485
  }
487
486
 
488
- function detectShiverFindings(propStats: any, elements: any, existingFindings: DejitterFinding[]): DejitterFinding[] {
487
+ function detectShiverFindings(propStats: any, elements: any): DejitterFinding[] {
489
488
  const findings: DejitterFinding[] = [];
490
489
 
491
490
  for (const p of propStats.props) {
492
491
  if (p.raw < 10) continue;
493
- if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
494
492
 
495
493
  const timeline = getTimeline(p.elem, p.prop);
496
494
  if (timeline.length < 10) continue;
@@ -512,7 +510,7 @@ export function createDejitterRecorder(): DejitterAPI {
512
510
 
513
511
  const vals = numeric.map((n) => n.val);
514
512
  const amplitude = Math.round((Math.max(...vals) - Math.min(...vals)) * 10) / 10;
515
- const hz = Math.round((reversals / ((numeric[numeric.length - 1].t - numeric[0].t) / 1000)) * 10) / 10;
513
+ const hz = Math.round((reversals / ((numeric[numeric.length - 1]!.t - numeric[0]!.t) / 1000)) * 10) / 10;
516
514
 
517
515
  findings.push(makeFinding(
518
516
  'shiver',
@@ -532,7 +530,7 @@ export function createDejitterRecorder(): DejitterAPI {
532
530
  range: [Math.min(...vals), Math.max(...vals)],
533
531
  uniqueValues: uniqueVals.length,
534
532
  isTwoValueFight,
535
- durationMs: Math.round(numeric[numeric.length - 1].t - numeric[0].t),
533
+ durationMs: Math.round(numeric[numeric.length - 1]!.t - numeric[0]!.t),
536
534
  },
537
535
  }
538
536
  ));
@@ -542,33 +540,32 @@ export function createDejitterRecorder(): DejitterAPI {
542
540
  return findings;
543
541
  }
544
542
 
545
- function detectJumpFindings(propStats: any, elements: any, existingFindings: DejitterFinding[]): DejitterFinding[] {
543
+ function detectJumpFindings(propStats: any, elements: any): DejitterFinding[] {
546
544
  const findings: DejitterFinding[] = [];
547
545
 
548
546
  for (const p of propStats.props) {
549
547
  if (p.raw < 3) continue;
550
- if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
551
548
 
552
549
  const timeline = getTimeline(p.elem, p.prop);
553
550
  if (timeline.length < 3) continue;
554
551
 
555
552
  const deltas: Array<{ t: number; delta: number; from: any; to: any }> = [];
556
553
  for (let i = 1; i < timeline.length; i++) {
557
- const prev = extractNumeric(timeline[i - 1].value);
558
- const curr = extractNumeric(timeline[i].value);
554
+ const prev = extractNumeric(timeline[i - 1]!.value);
555
+ const curr = extractNumeric(timeline[i]!.value);
559
556
  if (prev === null || curr === null) continue;
560
557
  deltas.push({
561
- t: timeline[i].t,
558
+ t: timeline[i]!.t,
562
559
  delta: Math.abs(curr - prev),
563
- from: timeline[i - 1].value,
564
- to: timeline[i].value,
560
+ from: timeline[i - 1]!.value,
561
+ to: timeline[i]!.value,
565
562
  });
566
563
  }
567
564
 
568
565
  if (deltas.length < 3) continue;
569
566
 
570
567
  const sortedDeltas = deltas.map((d) => d.delta).sort((a, b) => a - b);
571
- const medianDelta = sortedDeltas[Math.floor(sortedDeltas.length / 2)];
568
+ const medianDelta = sortedDeltas[Math.floor(sortedDeltas.length / 2)]!;
572
569
  if (medianDelta === 0) continue;
573
570
 
574
571
  const jmpT = config.thresholds.jump;
@@ -598,13 +595,12 @@ export function createDejitterRecorder(): DejitterAPI {
598
595
  return findings;
599
596
  }
600
597
 
601
- function detectStutterFindings(propStats: any, elements: any, existingFindings: DejitterFinding[]): DejitterFinding[] {
598
+ function detectStutterFindings(propStats: any, elements: any): DejitterFinding[] {
602
599
  const findings: DejitterFinding[] = [];
603
600
  const st = config.thresholds.stutter;
604
601
 
605
602
  for (const p of propStats.props) {
606
603
  if (p.raw < 6) continue;
607
- if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
608
604
 
609
605
  const timeline = getTimeline(p.elem, p.prop);
610
606
  if (timeline.length < 6) continue;
@@ -618,7 +614,7 @@ export function createDejitterRecorder(): DejitterAPI {
618
614
 
619
615
  const deltas: number[] = [];
620
616
  for (let i = 1; i < numeric.length; i++) {
621
- deltas.push(numeric[i].val - numeric[i - 1].val);
617
+ deltas.push(numeric[i]!.val - numeric[i - 1]!.val);
622
618
  }
623
619
 
624
620
  const windowSize = 5;
@@ -628,31 +624,31 @@ export function createDejitterRecorder(): DejitterAPI {
628
624
  if (i - winStart < 2) { i++; continue; }
629
625
 
630
626
  let sum = 0;
631
- for (let w = winStart; w < i; w++) sum += deltas[w];
627
+ for (let w = winStart; w < i; w++) sum += deltas[w]!;
632
628
  const dominantDir = Math.sign(sum);
633
629
  if (dominantDir === 0) { i++; continue; }
634
630
 
635
- if (deltas[i] !== 0 && Math.sign(deltas[i]) !== dominantDir) {
631
+ if (deltas[i]! !== 0 && Math.sign(deltas[i]!) !== dominantDir) {
636
632
  const reversalStart = i;
637
633
  let reversalEnd = i;
638
634
  while (
639
635
  reversalEnd + 1 < deltas.length &&
640
636
  reversalEnd - reversalStart + 1 < st.maxFrames &&
641
- deltas[reversalEnd + 1] !== 0 &&
642
- Math.sign(deltas[reversalEnd + 1]) !== dominantDir
637
+ deltas[reversalEnd + 1]! !== 0 &&
638
+ Math.sign(deltas[reversalEnd + 1]!) !== dominantDir
643
639
  ) {
644
640
  reversalEnd++;
645
641
  }
646
642
 
647
643
  const afterIdx = reversalEnd + 1;
648
- if (afterIdx >= deltas.length || Math.sign(deltas[afterIdx]) !== dominantDir) {
644
+ if (afterIdx >= deltas.length || Math.sign(deltas[afterIdx]!) !== dominantDir) {
649
645
  i = reversalEnd + 1;
650
646
  continue;
651
647
  }
652
648
 
653
649
  let reversalMag = 0;
654
650
  for (let r = reversalStart; r <= reversalEnd; r++) {
655
- reversalMag += Math.abs(deltas[r]);
651
+ reversalMag += Math.abs(deltas[r]!);
656
652
  }
657
653
 
658
654
  const localStart = Math.max(0, reversalStart - windowSize);
@@ -661,7 +657,7 @@ export function createDejitterRecorder(): DejitterAPI {
661
657
  let localCount = 0;
662
658
  for (let l = localStart; l <= localEnd; l++) {
663
659
  if (l >= reversalStart && l <= reversalEnd) continue;
664
- localSum += Math.abs(deltas[l]);
660
+ localSum += Math.abs(deltas[l]!);
665
661
  localCount++;
666
662
  }
667
663
  const localVelocity = localCount > 0 ? localSum / localCount : 0;
@@ -671,7 +667,7 @@ export function createDejitterRecorder(): DejitterAPI {
671
667
  if (ratio >= st.velocityRatio) {
672
668
  const reversalFrameCount = reversalEnd - reversalStart + 1;
673
669
  const severity = ratio >= 1.0 ? 'high' : ratio >= 0.5 ? 'medium' : 'low';
674
- const t = numeric[reversalStart + 1].t;
670
+ const t = numeric[reversalStart + 1]!.t;
675
671
  findings.push(makeFinding(
676
672
  'stutter', severity,
677
673
  p.elem, elements[p.elem], p.prop,
@@ -701,13 +697,12 @@ export function createDejitterRecorder(): DejitterAPI {
701
697
  return findings;
702
698
  }
703
699
 
704
- function detectStuckFindings(propStats: any, elements: any, existingFindings: DejitterFinding[]): DejitterFinding[] {
700
+ function detectStuckFindings(propStats: any, elements: any): DejitterFinding[] {
705
701
  const findings: DejitterFinding[] = [];
706
702
  const sk = config.thresholds.stuck;
707
703
 
708
704
  for (const p of propStats.props) {
709
705
  if (p.raw < 6) continue;
710
- if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
711
706
 
712
707
  const timeline = getTimeline(p.elem, p.prop);
713
708
  if (timeline.length < 6) continue;
@@ -721,15 +716,15 @@ export function createDejitterRecorder(): DejitterAPI {
721
716
 
722
717
  const deltas: number[] = [];
723
718
  for (let i = 1; i < numeric.length; i++) {
724
- deltas.push(Math.abs(numeric[i].val - numeric[i - 1].val));
719
+ deltas.push(Math.abs(numeric[i]!.val - numeric[i - 1]!.val));
725
720
  }
726
721
 
727
722
  let i = 0;
728
723
  while (i < deltas.length) {
729
- if (deltas[i] > sk.maxDelta) { i++; continue; }
724
+ if (deltas[i]! > sk.maxDelta) { i++; continue; }
730
725
 
731
726
  const runStart = i;
732
- while (i < deltas.length && deltas[i] <= sk.maxDelta) i++;
727
+ while (i < deltas.length && deltas[i]! <= sk.maxDelta) i++;
733
728
  const runEnd = i - 1;
734
729
  const stillCount = runEnd - runStart + 1;
735
730
 
@@ -741,13 +736,13 @@ export function createDejitterRecorder(): DejitterAPI {
741
736
 
742
737
  const beforeStart = Math.max(0, runStart - windowSize);
743
738
  for (let b = beforeStart; b < runStart; b++) {
744
- surroundingSum += deltas[b];
739
+ surroundingSum += deltas[b]!;
745
740
  surroundingCount++;
746
741
  }
747
742
 
748
743
  const afterEnd = Math.min(deltas.length - 1, runEnd + windowSize);
749
744
  for (let a = runEnd + 1; a <= afterEnd; a++) {
750
- surroundingSum += deltas[a];
745
+ surroundingSum += deltas[a]!;
751
746
  surroundingCount++;
752
747
  }
753
748
 
@@ -756,8 +751,8 @@ export function createDejitterRecorder(): DejitterAPI {
756
751
 
757
752
  if (meanSurroundingVelocity < sk.minSurroundingVelocity) continue;
758
753
 
759
- const tStart = numeric[runStart].t;
760
- const tEnd = numeric[runEnd + 1].t;
754
+ const tStart = numeric[runStart]!.t;
755
+ const tEnd = numeric[runEnd + 1]!.t;
761
756
  const duration = Math.round(tEnd - tStart);
762
757
 
763
758
  const severity = duration >= sk.highDuration ? 'high' : duration >= sk.medDuration ? 'medium' : 'low';
@@ -773,7 +768,7 @@ export function createDejitterRecorder(): DejitterAPI {
773
768
  duration,
774
769
  stillFrames: stillCount,
775
770
  meanSurroundingVelocity: Math.round(meanSurroundingVelocity * 10) / 10,
776
- stuckValue: numeric[runStart].val,
771
+ stuckValue: numeric[runStart]!.val,
777
772
  },
778
773
  }
779
774
  ));
@@ -797,10 +792,10 @@ export function createDejitterRecorder(): DejitterAPI {
797
792
  const deduped: DejitterFinding[] = [];
798
793
  for (const group of shiverGroups.values()) {
799
794
  if (group.length === 1) {
800
- deduped.push(group[0]);
795
+ deduped.push(group[0]!);
801
796
  } else {
802
797
  group.sort((a, b) => b.shiver.amplitude - a.shiver.amplitude);
803
- const rep = { ...group[0] };
798
+ const rep = { ...group[0]! };
804
799
  (rep as any).affectedElements = group.length;
805
800
  rep.description += ` (affects ${group.length} elements)`;
806
801
  deduped.push(rep);
@@ -815,14 +810,14 @@ export function createDejitterRecorder(): DejitterAPI {
815
810
  const elements = buildElementMap();
816
811
 
817
812
  let findings = detectOutlierFindings(propStats, elements);
818
- findings = findings.concat(detectShiverFindings(propStats, elements, findings));
819
- findings = findings.concat(detectJumpFindings(propStats, elements, findings));
820
- findings = findings.concat(detectStutterFindings(propStats, elements, findings));
821
- findings = findings.concat(detectStuckFindings(propStats, elements, findings));
813
+ findings = findings.concat(detectShiverFindings(propStats, elements));
814
+ findings = findings.concat(detectJumpFindings(propStats, elements));
815
+ findings = findings.concat(detectStutterFindings(propStats, elements));
816
+ findings = findings.concat(detectStuckFindings(propStats, elements));
822
817
  findings = deduplicateShivers(findings);
823
818
 
824
819
  const sevOrder: Record<string, number> = { high: 0, medium: 1, low: 2, info: 3 };
825
- findings.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
820
+ findings.sort((a, b) => sevOrder[a.severity]! - sevOrder[b.severity]!);
826
821
 
827
822
  return findings;
828
823
  }
@@ -891,7 +886,7 @@ export function createDejitterRecorder(): DejitterAPI {
891
886
 
892
887
  return {
893
888
  config: { ...config },
894
- duration: rawFrames.length ? rawFrames[rawFrames.length - 1].t : 0,
889
+ duration: rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0,
895
890
  rawFrameCount: rawFrames.length,
896
891
  outputFrameCount: samples.length,
897
892
  mutationEvents: mutations.length,
@@ -910,7 +905,7 @@ export function createDejitterRecorder(): DejitterAPI {
910
905
  byMode[p.mode] = (byMode[p.mode] || 0) + 1;
911
906
  }
912
907
  const data: DejitterSummary = {
913
- duration: rawFrames.length ? rawFrames[rawFrames.length - 1].t : 0,
908
+ duration: rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0,
914
909
  rawFrameCount: rawFrames.length,
915
910
  targetOutputFrames: propStats.targetFrames,
916
911
  mutationEvents: mutations.length,
@@ -3,6 +3,7 @@ import {
3
3
  AncestryItem,
4
4
  formatAncestryChain,
5
5
  collectAncestry,
6
+ truncateAtFirstFile,
6
7
  } from "./formatAncestryChain";
7
8
 
8
9
  describe("formatAncestryChain", () => {
@@ -266,4 +267,50 @@ describe("formatAncestryChain", () => {
266
267
  );
267
268
  });
268
269
  });
270
+
271
+ describe("truncateAtFirstFile", () => {
272
+ it("keeps from clicked element up to first ancestor with filePath", () => {
273
+ // Bottom-up: clicked element first, root last
274
+ const items: AncestryItem[] = [
275
+ { elementName: "span", componentName: "Button" },
276
+ { elementName: "div", componentName: "Card" },
277
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
278
+ { elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
279
+ ];
280
+
281
+ const result = truncateAtFirstFile(items);
282
+ expect(result).toEqual([
283
+ { elementName: "span", componentName: "Button" },
284
+ { elementName: "div", componentName: "Card" },
285
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
286
+ ]);
287
+ });
288
+
289
+ it("returns just the clicked element when it already has a filePath", () => {
290
+ const items: AncestryItem[] = [
291
+ { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
292
+ { elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
293
+ { elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
294
+ ];
295
+
296
+ const result = truncateAtFirstFile(items);
297
+ expect(result).toEqual([
298
+ { elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
299
+ ]);
300
+ });
301
+
302
+ it("returns all items when none have a filePath", () => {
303
+ const items: AncestryItem[] = [
304
+ { elementName: "span", componentName: "A" },
305
+ { elementName: "div", componentName: "B" },
306
+ ];
307
+
308
+ const result = truncateAtFirstFile(items);
309
+ expect(result).toEqual(items);
310
+ });
311
+
312
+ it("returns empty array for empty input", () => {
313
+ expect(truncateAtFirstFile([])).toEqual([]);
314
+ });
315
+ });
269
316
  });
@@ -164,6 +164,17 @@ function getInnermostNamedComponent(item: AncestryItem | null | undefined): stri
164
164
  return undefined;
165
165
  }
166
166
 
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.
170
+ * The ancestry array is bottom-up: index 0 = clicked element, last = root.
171
+ */
172
+ 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);
176
+ }
177
+
167
178
  export function formatAncestryChain(items: AncestryItem[]): string {
168
179
  if (items.length === 0) {
169
180
  return "";
@@ -1,26 +1,5 @@
1
1
  import { Fiber } from "@locator/shared";
2
2
 
3
- // function printDebugOwnerTree(fiber: Fiber): string | null {
4
- // let current: Fiber | null = fiber || null;
5
- // let results = [];
6
- // while (current) {
7
- // results.push(getUsableName(current));
8
- // current = current._debugOwner || null;
9
- // }
10
- // console.log('DEBUG OWNER: ', results);
11
- // return null;
12
- // }
13
- // function printReturnTree(fiber: Fiber): string | null {
14
- // let current: Fiber | null = fiber || null;
15
- // let results = [];
16
- // while (current) {
17
- // results.push(getUsableName(current));
18
- // current = current.return || null;
19
- // }
20
- // console.log('RETURN: ', results);
21
- // return null;
22
- // }
23
-
24
3
  export function getUsableName(fiber: Fiber | null | undefined): string {
25
4
  if (!fiber) {
26
5
  return "Not found";
@@ -1,4 +1,4 @@
1
- import { Fiber, Target } from "@locator/shared";
1
+ import { Target } from "@locator/shared";
2
2
 
3
3
  export type Source = {
4
4
  fileName: string;
@@ -7,35 +7,6 @@ export type Source = {
7
7
  projectPath?: string;
8
8
  };
9
9
 
10
- type SimpleElement = {
11
- type: "element";
12
- name: string;
13
- uniqueId: string;
14
- fiber: Fiber;
15
- box: SimpleDOMRect | null;
16
- element: Element | Text;
17
- children: (SimpleElement | SimpleComponent)[];
18
- source: Source | null;
19
- };
20
-
21
- type SimpleComponent = {
22
- type: "component";
23
- uniqueId: string;
24
- name: string;
25
- fiber: Fiber;
26
- box: SimpleDOMRect | null;
27
- children: (SimpleElement | SimpleComponent)[];
28
- source: Source | null;
29
- definitionSourceFile: string | null;
30
- };
31
-
32
- export type SimpleNode = SimpleElement | SimpleComponent;
33
-
34
- export type HighlightedNode = {
35
- getNode: () => SimpleNode | null;
36
- setNode: (node: SimpleNode | null) => void;
37
- };
38
-
39
10
  export type SimpleDOMRect = {
40
11
  height: number;
41
12
  width: number;
@@ -51,5 +22,3 @@ export type LinkProps = {
51
22
  line: number;
52
23
  column: number;
53
24
  };
54
-
55
- export type ContextMenuState = { target: HTMLElement; x: number; y: number };