@progen-labs/spectra 0.1.0 → 0.1.2
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/README.md +3 -2
- package/dist/assets/index-COnXcwA_.js +11 -0
- package/dist/assets/index-DxxJOPn9.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/server/analysis/findings.js +34 -15
- package/server/analysis/report.js +27 -2
- package/server/chrome.js +1 -1
- package/server/probe.browser.js +142 -3
- package/src/App.jsx +1 -1
- package/src/components/ReactPanel.jsx +10 -4
- package/dist/assets/index-BvBsPok5.js +0 -46
- package/dist/assets/index-Da3Le5Px.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--font-ui:-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;--font-mono:ui-monospace, "SF Mono", Menlo, Consolas, monospace;--radius:14px;--radius-sm:9px;--btn-radius:9px;--blur:none;--on-accent:#fff;--label-case:none;--label-tracking:0}:root[data-theme=glass]{--radius:22px;--radius-sm:12px;--btn-radius:999px;--blur:blur(28px) saturate(180%);--font-ui:-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif}:root[data-theme=glass][data-mode=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:radial-gradient(900px 600px at 8% -8%, #b9dcff 0%, transparent 62%), radial-gradient(800px 560px at 96% 4%, #ffd3ea 0%, transparent 58%), radial-gradient(1000px 700px at 62% 112%, #d6cfff 0%, transparent 62%), #eaeff7;--panel:#ffffff80;--panel-solid:#f4f7fc;--panel-border:#ffffffbf;--panel-edge:inset 0 1px 0 #ffffffe6, inset 0 -1px 0 #7887aa14;--shadow:0 1px 2px #283c6e0f, 0 12px 36px #283c6e1f;--shadow-pop:0 4px 12px #283c6e1f, 0 24px 60px #283c6e38;--surface:#f6f8fc;--surface-2:#ffffff9e;--control:#ffffffb3;--control-hover:#fffffff2;--border:#46557d29;--text:#0e1220;--text-2:#444c5e;--text-muted:#6d7588;--grid:#46557d1a;--accent:#0a67d1;--accent-strong:#0a6fdb;--accent-soft:#0a6fdb1f;--accent-glow:0 6px 18px #0a6fdb59;--heat:#f2602f;--cat-1:#0a6fdb;--cat-2:#f2602f;--cat-3:#1fa971;--cat-4:#eba000;--cat-5:#e5679b;--cat-6:#5b54d6;--cat-other:#a3a9b8;--good:#1a7f37;--warn:#a35607;--poor:#c9302c;--code-string:#17754f;--code-keyword:#5b54d6;--code-number:#a35607}:root[data-theme=glass][data-mode=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:radial-gradient(900px 600px at 6% -10%, #2463eb80 0%, transparent 62%), radial-gradient(800px 560px at 98% 0%, #a838c861 0%, transparent 58%), radial-gradient(1000px 700px at 60% 115%, #1496aa4d 0%, transparent 62%), #0a0c13;--panel:#22263685;--panel-solid:#161924;--panel-border:#ffffff24;--panel-edge:inset 0 1px 0 #ffffff2e, inset 0 -1px 0 #ffffff08;--shadow:0 1px 2px #0000004d, 0 16px 44px #00000061;--shadow-pop:0 4px 14px #0006, 0 28px 70px #0000008c;--surface:#12141d;--surface-2:#ffffff0f;--control:#ffffff17;--control-hover:#ffffff29;--border:#ffffff21;--text:#f4f6fb;--text-2:#b8bfd0;--text-muted:#8990a3;--grid:#ffffff12;--accent:#4da3ff;--accent-strong:#0a6fdb;--accent-soft:#4da3ff2e;--accent-glow:0 6px 22px #0a6fdb80;--heat:#ff7a4d;--cat-1:#4da3ff;--cat-2:#ff7a4d;--cat-3:#30c98a;--cat-4:#f0b429;--cat-5:#f06ea0;--cat-6:#9a93f5;--cat-other:#6c7386;--good:#3fcf6b;--warn:#f0b04a;--poor:#ff6b6b;--code-string:#3fd49b;--code-keyword:#a29bf7;--code-number:#f0b04a}:root[data-theme=space]{--radius:12px;--radius-sm:8px;--btn-radius:8px;--blur:blur(10px);--label-case:uppercase;--label-tracking:.08em;--font-ui:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif}:root[data-theme=space][data-mode=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:radial-gradient(1px 1px at 22px 34px, #ffffffe6, transparent), radial-gradient(1px 1px at 96px 128px, #fff9, transparent), radial-gradient(1.5px 1.5px at 168px 62px, #a0c8ffe6, transparent), radial-gradient(1px 1px at 58px 176px, #ffffff80, transparent), radial-gradient(1.5px 1.5px at 190px 190px, #c8b4ffcc, transparent), radial-gradient(900px 620px at 82% -12%, #7c5cff4d, transparent 62%), radial-gradient(800px 600px at -6% 108%, #00beff2e, transparent 60%), #04050d;--bg-size:210px 210px, 210px 210px, 210px 210px, 210px 210px, 210px 210px, auto, auto, auto;--panel:#0c1024bd;--panel-solid:#0b0f24;--panel-border:#6e82ff3d;--panel-edge:inset 0 1px 0 #a0b4ff1a;--shadow:0 0 0 1px #0006, 0 18px 50px #0000008c;--shadow-pop:0 0 0 1px #6e82ff40, 0 0 40px #6464ff2e, 0 28px 70px #000000b3;--surface:#0a0d20;--surface-2:#6e82ff12;--control:#6e82ff1a;--control-hover:#6e82ff33;--border:#6e82ff38;--text:#eaeefc;--text-2:#aab3d6;--text-muted:#7b85ad;--grid:#6e82ff1a;--accent:#6f9bff;--accent-strong:#4565f0;--accent-soft:#6f9bff29;--accent-glow:0 0 22px #506eff8c;--accent-2:#22d3ee;--heat:#ff8a3d;--cat-1:#6f9bff;--cat-2:#ff8a3d;--cat-3:#2dd4a0;--cat-4:#fbbf24;--cat-5:#f472b6;--cat-6:#a78bfa;--cat-other:#5f6890;--good:#34d98b;--warn:#fbbf24;--poor:#ff6b81;--code-string:#34d9a0;--code-keyword:#b49bff;--code-number:#fbbf24}:root[data-theme=space][data-mode=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:radial-gradient(1px 1px at 22px 34px, #4f46e559, transparent), radial-gradient(1px 1px at 120px 130px, #0e74904d, transparent), radial-gradient(1.5px 1.5px at 170px 70px, #4f46e54d, transparent), radial-gradient(900px 600px at 84% -12%, #818cf852, transparent 62%), radial-gradient(800px 600px at -6% 108%, #22d3ee33, transparent 60%), #eef0fa;--bg-size:210px 210px, 210px 210px, 210px 210px, auto, auto, auto;--panel:#fffc;--panel-solid:#f8f9ff;--panel-border:#4f46e533;--panel-edge:inset 0 1px 0 #ffffffe6;--shadow:0 1px 2px #1e1b640f, 0 14px 40px #1e1b641f;--shadow-pop:0 0 0 1px #4f46e526, 0 24px 60px #1e1b643d;--surface:#f8f9ff;--surface-2:#4f46e50d;--control:#ffffffe6;--control-hover:#fff;--border:#4f46e533;--text:#0f1226;--text-2:#424a6b;--text-muted:#6a7192;--grid:#4f46e51a;--accent:#4338ca;--accent-strong:#4f46e5;--accent-soft:#4f46e51a;--accent-glow:0 6px 20px #4f46e559;--accent-2:#0e7490;--heat:#e0560c;--cat-1:#4f46e5;--cat-2:#e0560c;--cat-3:#0f9f78;--cat-4:#d99100;--cat-5:#d6458a;--cat-6:#7c3aed;--cat-other:#9aa0bd;--good:#14803f;--warn:#9a5200;--poor:#c62839;--code-string:#0f7a5a;--code-keyword:#6d3fd6;--code-number:#9a5200}:root[data-theme=glass] button.primary,:root[data-theme=glass] .tabs button[aria-selected=true]{background:linear-gradient(180deg, #ffffff47, transparent 55%), var(--accent-strong)}:root[data-theme=space] button.primary{background:linear-gradient(135deg, var(--accent-strong), #8b5cf6);border-color:#0000}:root[data-theme=space] .brand-mark{background:conic-gradient(from 210deg,#22d3ee,#6f6bff,#c084fc,#22d3ee)}:root[data-theme=glass] .brand-mark{background:linear-gradient(135deg,#5ac8fa,#0a84ff 55%,#bf5af2)}*{box-sizing:border-box;scrollbar-width:thin;scrollbar-color:var(--border) transparent}html,body,#root{height:100%;margin:0}body{background:var(--bg);background-size:var(--bg-size,auto);color:var(--text);font:.8125rem/1.45 var(--font-ui);-webkit-font-smoothing:antialiased;background-attachment:fixed}button,input,select{font:inherit;color:inherit}button,select,input:not([type=checkbox]):not([type=radio]){background:var(--control);border:1px solid var(--border);border-radius:var(--radius-sm);min-height:2rem;padding:.35rem .75rem;transition:background .15s,border-color .15s,box-shadow .15s}button{border-radius:var(--btn-radius);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:.4rem;font-weight:500;display:inline-flex}select{cursor:pointer}input:not([type=checkbox]):not([type=radio]):focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}button:hover:not(:disabled),select:hover:not(:disabled){background:var(--control-hover);border-color:color-mix(in srgb, var(--accent) 55%, var(--border))}button:disabled,select:disabled,input:disabled{opacity:.5;cursor:default}button.primary{background:var(--accent-strong);color:var(--on-accent);box-shadow:var(--accent-glow);border-color:#0000}button.primary:hover:not(:disabled){background:var(--accent-strong);filter:brightness(1.1);border-color:#0000}button.danger{background:var(--poor);color:#fff;border-color:#0000}button.danger:hover:not(:disabled){background:var(--poor);filter:brightness(1.08);border-color:#0000}button.ghost{color:var(--text-2);background:0 0;border-color:#0000}button.ghost:hover:not(:disabled){background:var(--control);border-color:var(--border);color:var(--text)}button.ghost.destructive:hover:not(:disabled){color:var(--poor)}button.link{min-height:0;color:var(--accent);text-align:left;font-weight:inherit;background:0 0;border:0;padding:0;display:inline}button.link:hover:not(:disabled){background:0 0;text-decoration:underline}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}code,.mono{font-family:var(--font-mono);font-size:12px}.muted{color:var(--text-muted);font-weight:400}.warn{color:var(--warn)}.spacer{flex:1}.pad{padding:1rem}.num{text-align:right;font-variant-numeric:tabular-nums}.empty{text-align:center;color:var(--text-muted);flex:1;place-content:center;gap:.4rem;padding:2rem;display:grid}.icon{flex:none}.app{flex-direction:column;gap:.7rem;height:100%;padding:.75rem;display:flex}.topbar,.session,.workspace{background:var(--panel);-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow), var(--panel-edge)}.topbar{z-index:10;align-items:center;gap:.8rem;padding:.55rem .7rem .55rem .9rem;display:flex;position:relative}.brand{align-items:center;gap:.7rem;min-width:0;display:flex}.brand-mark{border-radius:calc(var(--radius-sm) - 1px);color:#fff;place-items:center;width:2rem;height:2rem;display:grid;box-shadow:inset 0 1px #fff6,0 4px 14px #2850dc59}.brand h1{letter-spacing:-.01em;margin:0;font-size:.95rem;font-weight:650;line-height:1.2}.brand small{color:var(--text-muted);font-size:.72rem;font-family:var(--font-mono);display:block}.report-picker{align-items:center;gap:.3rem;display:inline-flex}.report-picker select{max-width:24rem}.error{background:color-mix(in srgb, var(--poor) 14%, var(--panel-solid));border:1px solid color-mix(in srgb, var(--poor) 55%, transparent);border-radius:var(--radius-sm);align-items:center;gap:.8rem;padding:.5rem .8rem;display:flex}.error button{margin-left:auto}.popover{display:inline-block;position:relative}.caret{opacity:.7;font-size:.7em}.popover-panel,.trace-note{background:color-mix(in srgb, var(--panel-solid) 98%, transparent);-webkit-backdrop-filter:blur(24px)saturate(160%);backdrop-filter:blur(24px)saturate(160%);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow-pop), var(--panel-edge)}.popover-panel{z-index:40;min-width:15rem;padding:.9rem;position:absolute;top:calc(100% + 8px)}.popover-panel.align-right{right:0}.popover-panel.align-left{left:0}.appearance,.options-list{gap:1rem;width:17rem;display:grid}.choice-group{border:0;min-width:0;margin:0;padding:0}.choice-group legend,.field-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.4rem;padding:0;font-size:.7rem;font-weight:600}.choice input{opacity:0;pointer-events:none;position:absolute}.choice:focus-within{outline:2px solid var(--accent);outline-offset:2px}.theme-cards{grid-template-columns:1fr 1fr;gap:.5rem;display:grid}.theme-cards .choice{border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:var(--surface-2);gap:.15rem;padding:.45rem;transition:border-color .15s,box-shadow .15s;display:grid;position:relative}.theme-cards .choice:hover{border-color:color-mix(in srgb, var(--accent) 55%, var(--border))}.theme-cards .choice.selected{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.theme-cards .choice-label{margin-top:.25rem;font-weight:600}.theme-cards small{font-size:.7rem;line-height:1.25}.swatch{border-radius:calc(var(--radius-sm) - 3px);height:2.9rem;display:block;position:relative;overflow:hidden}.swatch i{border-radius:999px;height:.5rem;position:absolute;left:.45rem}.swatch i:first-child{width:55%;top:.5rem}.swatch i:last-child{width:35%;top:1.35rem}.swatch-glass{background:radial-gradient(circle at 20% 20%,#8ec5ff,#0000 60%),radial-gradient(circle at 85% 30%,#ffb8df,#0000 55%),linear-gradient(135deg,#c9bcff,#9fd6ff)}.swatch-glass i{background:#ffffffb3;box-shadow:inset 0 1px #fff}.swatch-space{background:radial-gradient(1px 1px at 70% 25%,#fff,#0000),radial-gradient(1px 1px at 88% 70%,#bcd4ff,#0000),radial-gradient(1px 1px at 45% 80%,#fff,#0000),radial-gradient(circle at 90% 0,#7c5cff99,#0000 60%),#05071a}.swatch-space i{background:linear-gradient(90deg,#22d3ee,#6f6bff)}.segmented,.segmented-control{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--btn-radius);gap:2px;padding:3px;display:inline-flex}.segmented-control{display:flex}.segmented button,.segmented-control .choice{border-radius:calc(var(--btn-radius) - 2px);min-height:1.7rem;color:var(--text-2);text-align:center;cursor:pointer;background:0 0;border:0;flex:1;padding:.15rem .8rem;font-weight:500;position:relative}.segmented-control .choice{place-items:center;display:grid}.segmented button:hover:not(:disabled){background:var(--control);border:0}.segmented button[aria-pressed=true],.segmented-control .choice.selected{background:var(--accent-soft);color:var(--accent);font-weight:600}label.switch{cursor:pointer;color:var(--text-2);-webkit-user-select:none;user-select:none;align-items:center;gap:.5rem;display:inline-flex}.switch input{appearance:none;background:var(--control);border:1px solid var(--border);cursor:pointer;border-radius:999px;flex:none;width:2rem;height:1.15rem;margin:0;transition:background .15s;position:relative}.switch input:after{content:"";background:var(--text-muted);border-radius:50%;width:calc(1.15rem - 4px);height:calc(1.15rem - 4px);transition:transform .15s,background .15s;position:absolute;top:1px;left:1px}.switch input:checked{background:var(--accent-strong);border-color:#0000}.switch input:checked:after{background:#fff;transform:translate(.85rem)}.switch input:disabled{cursor:default}.session{z-index:5;gap:.6rem;padding:.7rem .8rem;display:grid;position:relative}.session-row{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.session-row+.session-row{border-top:1px solid var(--border);padding-top:.6rem}.session label,.connect label{color:var(--text-2)}.address{min-width:14rem;font-family:var(--font-mono);flex:1;font-size:12px}.btn-group{align-items:center;gap:.4rem;display:inline-flex}.connect{gap:.7rem;display:grid}.connect-head h2{letter-spacing:-.01em;margin:0;font-size:1rem}.connect-head p{margin:.15rem 0 0}.connect-main{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.connect-main .field{flex:1;min-width:16rem}.field input{width:100%}.field-row input{flex:1;min-width:0}.advanced{border-top:1px solid var(--border);gap:.8rem;padding-top:.7rem;display:grid}.advanced-section{gap:.4rem;display:grid}.advanced-section+.advanced-section{border-top:1px dashed var(--border);padding-top:.7rem}.advanced-row{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.advanced-row .field{flex:1;min-width:16rem}.pill{border:1px solid var(--border);background:var(--surface-2);white-space:nowrap;border-radius:999px;align-items:center;gap:.35rem;padding:.1rem .65rem;font-size:.75rem;display:inline-flex}.pill.ok{color:var(--good);border-color:color-mix(in srgb, var(--good) 40%, transparent)}.pill.warn{color:var(--warn);border-color:color-mix(in srgb, var(--warn) 45%, transparent)}.rec-strip{flex-wrap:wrap;flex:1;align-items:center;gap:.8rem;display:flex}.rec-strip.recording{border-radius:var(--radius-sm);background:color-mix(in srgb, var(--poor) 10%, transparent);border:1px solid color-mix(in srgb, var(--poor) 35%, transparent);padding:.4rem .6rem}.rec{align-items:center;gap:.5rem;font-weight:600;display:inline-flex}.rec i{background:var(--poor);width:10px;height:10px;box-shadow:0 0 0 4px color-mix(in srgb, var(--poor) 25%, transparent);border-radius:50%;animation:1s infinite pulse}.rec i.spin{background:var(--accent);box-shadow:0 0 0 4px var(--accent-soft)}.elapsed{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--text-2);font-weight:400}@keyframes pulse{50%{opacity:.3}}@media (prefers-reduced-motion:reduce){.rec i{animation:none}*{transition:none!important}}.workspace{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.report-bar{border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center;gap:.8rem;padding:.6rem .9rem;display:flex}.report-title{text-overflow:ellipsis;white-space:nowrap;max-width:28rem;font-size:.9rem;font-weight:600;overflow:hidden}.report-meta{flex-wrap:wrap;gap:.35rem;display:flex}.tabs{gap:.2rem;display:flex;overflow-x:auto}.main-tabs{border-bottom:1px solid var(--border);align-items:center;padding:.5rem .9rem}.main-tabs .tab-list{gap:.2rem;min-width:0;display:flex;overflow-x:auto}.tabs button{min-height:1.9rem;color:var(--text-2);background:0 0;border:0;padding:.25rem .8rem}.tabs button:hover:not(:disabled){background:var(--control);color:var(--text);border:0}.tabs button[aria-selected=true]{background:var(--accent-strong);color:var(--on-accent);box-shadow:var(--accent-glow)}.count{background:var(--accent-soft);color:var(--accent);border-radius:999px;margin-left:.1rem;padding:0 .45rem;font-size:11px;font-weight:600}.tabs button[aria-selected=true] .count{color:inherit;background:#ffffff40}.main{flex:1;min-height:0;display:flex}.content{flex-direction:column;flex:1;min-width:0;display:flex;overflow:auto}.welcome{text-align:center;flex:1;align-content:center;justify-items:center;gap:1.6rem;padding:2rem 1.5rem;display:grid;overflow:auto}.welcome h2{letter-spacing:-.02em;margin:0;font-size:1.6rem}.welcome-lead{color:var(--text-2);margin:.4rem 0 0;font-size:.95rem}.steps{text-align:left;grid-template-columns:repeat(3,minmax(0,17rem));gap:.9rem;margin:0;padding:0;list-style:none;display:grid}.steps li{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius);align-content:start;gap:.35rem;padding:1rem;line-height:1.55;display:grid}.step-num{background:var(--accent-soft);width:1.7rem;height:1.7rem;color:var(--accent);border-radius:50%;place-items:center;font-weight:700;display:grid}.steps h3{margin:.2rem 0 0;font-size:.9rem}.steps p{color:var(--text-2);margin:0}.welcome-foot{max-width:44rem;color:var(--text-muted);margin:0}@media (width<=900px){.steps{grid-template-columns:1fr}}.culprits{align-content:start;gap:1.2rem;padding:1rem 1.2rem;display:grid}.culprits h2,.network h2,.long-tasks h2{margin:0 0 .5rem;font-size:.9rem}.vital-label{color:var(--text-2);font-size:12px}.vital-value{font-variant-numeric:tabular-nums;font-size:1.25rem;font-weight:600}.vital-rating{font-size:12px}.rating-good{border-left-color:var(--good)}.rating-good .vital-rating{color:var(--good)}.rating-warn{border-left-color:var(--warn)}.rating-warn .vital-rating{color:var(--warn)}.rating-poor{border-left-color:var(--poor)}.rating-poor .vital-rating{color:var(--poor)}.rating-none{border-left-color:var(--border)}.rating-none .vital-rating{color:var(--text-muted)}.breakdown-bar{background:var(--surface);border-radius:4px;gap:2px;height:16px;display:flex;overflow:hidden}.breakdown-bar.small{height:8px;margin:.35rem 0}.breakdown-bar span{min-width:2px}.breakdown-legend{flex-wrap:wrap;gap:.4rem 1.2rem;margin:.5rem 0 0;padding:0;list-style:none;display:flex}.breakdown-legend i{border-radius:2px;width:10px;height:10px;margin-right:.35rem;display:inline-block}.sev-critical{border-left-color:var(--poor)}.sev-warning{border-left-color:var(--warn)}.sev-info{border-left-color:var(--accent)}.badge-sev{font-size:12px;font-weight:600}.sev-critical .badge-sev{color:var(--poor)}.sev-warning .badge-sev{color:var(--warn)}.sev-info .badge-sev{color:var(--accent)}.chip{border:1px solid var(--border);color:var(--text-2);border-radius:4px;padding:0 .4rem;font-size:11px}.chip-warn{color:var(--warn);border-color:var(--warn)}.tree-pane{flex-direction:column;flex:1;min-height:0;display:flex}.tree-toolbar{border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center;gap:.8rem;padding:.4rem 1rem;display:flex}.tree-svg{cursor:grab;background:radial-gradient(var(--grid) 1px, transparent 1px) 0 0 / 22px 22px;flex:1;width:100%;min-height:0;display:block}.tree-svg:active{cursor:grabbing}.tree-links path{fill:none;stroke:var(--border);stroke-width:1.2px}.node-hit{fill:#0000}.node:hover .node-hit{fill:var(--surface-2)}.node-dot{fill:color-mix(in srgb, var(--heat) var(--heat-pct,20%), var(--surface));stroke:var(--heat);stroke-width:1.5px;cursor:pointer}.node.vendor .node-dot{fill:var(--surface);stroke:var(--text-muted);stroke-dasharray:2 2}.node.dir .node-dot{stroke-width:2px}.node.selected .node-hit{fill:color-mix(in srgb, var(--accent) 22%, transparent)}.node.selected .node-label{font-weight:700}.node-glyph{fill:var(--text);pointer-events:none;font-size:10px}.node-label{fill:var(--text);cursor:pointer;font-size:12px}.node.dir .node-label{fill:var(--text-2);font-weight:600}.legend{border-top:1px solid var(--border);color:var(--text-2);flex-wrap:wrap;align-items:center;gap:1.2rem;padding:.4rem 1rem;display:flex}.ramp{vertical-align:middle;background:linear-gradient(90deg, color-mix(in srgb, var(--heat) 12%, var(--surface)), var(--heat));border:1px solid var(--heat);border-radius:2px;width:90px;height:10px;margin-right:.4rem;display:inline-block}.ring{border:1.5px dashed var(--text-muted);vertical-align:middle;border-radius:50%;width:10px;height:10px;margin-right:.35rem;display:inline-block}.file-panel{border-left:1px solid var(--border);flex-direction:column;flex:none;width:34rem;max-width:45vw;min-height:0;display:flex}.panel-head{border-bottom:1px solid var(--border);align-items:center;gap:.5rem;padding:.5rem .8rem;display:flex}.panel-head h2{text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-family:ui-monospace,Menlo,monospace;font-size:.8125rem;overflow:hidden}.metrics{grid-template-columns:repeat(3,1fr);gap:.4rem;padding:.6rem .8rem;display:grid}.metric{background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:.3rem .5rem}.metric-value{font-variant-numeric:tabular-nums;font-weight:600}.metric.warn .metric-value{color:var(--warn)}.metric-label{color:var(--text-muted);font-size:11px}.mini-findings{gap:.3rem;margin:0 .8rem .5rem;padding:0;list-style:none;display:grid}.mini-findings li{border-left:3px solid;padding-left:.5rem}.file-panel .tabs{border-bottom:1px solid var(--border);padding:0 .8rem}.panel-body{flex-direction:column;flex:1;min-height:0;display:flex;overflow:auto}table.data{border-collapse:collapse;width:100%}table.data th{text-align:left;color:var(--text-2);background:var(--panel-solid);border-bottom:1px solid var(--border);padding:.4rem .6rem;font-weight:600;position:sticky;top:0}table.data th button{color:inherit;background:0 0;border:0;padding:0;font-weight:600}table.data td{border-bottom:1px solid var(--grid);vertical-align:top;padding:.25rem .6rem}tr.open td,tr.detail td{background:var(--surface-2)}.detail-grid{grid-template-columns:1fr 2fr;gap:1.5rem;display:grid}.detail-grid h4{text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 .3rem;font-size:12px}.detail-grid ul{margin:0;padding-left:1.1rem}.summary-row{color:var(--text-2);border-bottom:1px solid var(--border);flex-wrap:wrap;gap:1.4rem;padding:.6rem 1rem;display:flex}.bar{background:var(--grid);border-radius:2px;height:3px;margin-top:3px}.bar span{background:var(--heat);border-radius:2px;height:100%;display:block}.react-panel,.network{overflow:auto}.network section{margin:1.2rem 0}.timeline{background:var(--surface-2);border-bottom:1px solid var(--border);width:100%;height:130px}.timeline .task{fill:var(--cat-other)}.timeline .task.long{fill:var(--poor)}.timeline .limit{stroke:var(--warn);stroke-dasharray:4 4;vector-effect:non-scaling-stroke}.timeline .axis{fill:var(--text-muted);font-size:11px}.task-list{gap:.7rem;margin:0;padding:.8rem 1rem;list-style:none;display:grid}.task-list>li{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:4px;padding:.6rem .9rem}.task-head{flex-wrap:wrap;align-items:center;gap:.7rem;display:flex}.task-cols{grid-template-columns:1fr 1fr;gap:1rem;display:grid}.task-cols h4{text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:.3rem 0;font-size:12px}.row{justify-content:space-between;gap:.6rem;width:100%;display:flex}.row span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.code-scroll{flex:1;align-items:flex-start;font:12px/18px ui-monospace,Menlo,Consolas,monospace;display:flex;overflow:auto}.gutter{background:var(--surface-2);border-right:1px solid var(--border);text-align:right;flex:none;position:sticky;left:0}.gutter-line{height:18px;color:var(--text-muted);white-space:nowrap;padding:0 .5rem}.gutter-hot{background:color-mix(in srgb, var(--heat) 22%, transparent);color:var(--text);font-weight:600}.code{flex:1;margin:0;padding:0 1rem}.code code.hljs{font:inherit;white-space:pre;background:0 0;padding:0;display:block}.hljs-comment,.hljs-quote{color:var(--text-muted);font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-literal{color:var(--code-keyword)}.hljs-string,.hljs-regexp,.hljs-attr{color:var(--code-string)}.hljs-number,.hljs-built_in,.hljs-type{color:var(--code-number)}.hljs-title,.hljs-function,.hljs-name{color:var(--accent)}.culprit-box{border:1px dashed var(--border);background:var(--surface);border-radius:6px;margin:.5rem 0 .2rem;padding:.5rem .7rem}.culprit-box h4{text-transform:uppercase;letter-spacing:.05em;color:var(--text-2);margin:0 0 .3rem;font-size:11px}.culprit-box ul{gap:.25rem;margin:0;padding:0;list-style:none;display:grid}code.fn{color:var(--text);font-weight:700}.code-ref{flex-wrap:wrap;align-items:baseline;gap:.4rem;display:inline-flex}.code-ref b{font-weight:700}.editor-link{color:var(--accent);border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-size:11px;text-decoration:none}.editor-link:hover{border-color:var(--accent)}table.hot-functions td{vertical-align:middle}tr.vendor-row code.fn{color:var(--text-2);font-weight:400}.trace-button{position:relative}.trace-note{z-index:40;width:30rem;max-width:80vw;padding:.8rem 2rem .8rem 1rem;line-height:1.5;position:absolute;top:calc(100% + 8px);right:0}.trace-note.error{border-color:var(--poor)}.trace-note ol{margin:.4rem 0 0;padding-left:1.2rem}.trace-note code{word-break:break-all}.trace-note .close{background:0 0;border:0;min-height:0;padding:0 .3rem;font-size:1.1rem;line-height:1;position:absolute;top:.4rem;right:.4rem}kbd{border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-size:11px}.compare-panel{align-content:start;gap:1rem;padding:1rem 1.2rem 2rem;display:grid}.compare-intro h2{margin:0 0 .3rem;font-size:1rem}.sides{grid-template-columns:1fr auto 1fr;align-items:start;gap:.8rem;display:grid}.swap{align-self:center}.swap button{padding:.3rem .6rem;font-size:1.1rem}.side-picker{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);gap:.5rem;padding:.7rem .9rem;display:grid}.side-picker h3{margin:0;font-size:.9rem}.field{color:var(--text-2);gap:.2rem;display:grid}.field-row{gap:.4rem;display:flex}.field input{flex:1;min-width:0}.git-facts{grid-template-columns:5.5rem 1fr;gap:.15rem .6rem;margin:0;display:grid}.git-facts dt{color:var(--text-muted)}.git-facts dd{overflow-wrap:anywhere;margin:0}.dropzone{border:1.5px dashed var(--border);border-radius:var(--radius-sm);flex-wrap:wrap;align-items:center;gap:.6rem;padding:.6rem;display:flex}.dropzone.dragging{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 10%, transparent)}.run-chips{gap:.3rem;margin:0;padding:0;list-style:none;display:grid}.run-chips li{background:var(--surface);border:1px solid var(--border);border-radius:6px;align-items:center;gap:.6rem;padding:.2rem .5rem;display:flex}.run-chips li .mono{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.run-chips button{background:0 0;border:0;padding:0 .3rem;font-size:1rem}.compare-toolbar{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);z-index:5;flex-wrap:wrap;align-items:center;gap:.6rem;padding:.5rem .7rem;display:flex;position:sticky;top:0}.pdf-buttons{align-items:center;gap:.5rem;display:inline-flex}.compare-results{gap:1.2rem;display:grid}.compare-results h2{margin:0 0 .5rem;font-size:.95rem}.verdict-banner{border:1.5px solid var(--c);border-radius:var(--radius-sm);border-left-width:6px;padding:.7rem 1rem}.verdict-banner .big{color:var(--c);font-size:1.4rem;font-weight:700}.verdict-banner p{margin:.25rem 0}.verdict-banner .counts span{margin-right:1.2rem;font-weight:600}.good{--c:var(--good)}.bad{--c:var(--poor)}.warn{--c:var(--warn)}.same{--c:var(--text-2)}.na{--c:var(--text-muted)}.info{--c:var(--accent)}.verdict-banner .counts .good,.verdict-banner .counts .bad,.verdict-banner .counts .same,.verdict{color:var(--c)}.verdict{white-space:nowrap;font-weight:700}ul.icons{gap:.25rem;margin:0;padding:0;list-style:none;display:grid}ul.icons li{color:var(--text)}ul.icons li .i,ul.checks li .i{width:1.4rem;color:var(--c);font-weight:700;display:inline-block}ul.checks{gap:.25rem;margin:0;padding:0;list-style:none;display:grid}ul.checks li{align-items:baseline;display:flex}.compare-table td,.compare-table th{white-space:nowrap}.compare-table td:first-child{white-space:normal}.compare-table tr.dim td{color:var(--text-muted)}tr.group-row td{background:var(--surface-2);text-transform:uppercase;letter-spacing:.05em;color:var(--text-2);font-size:11px;font-weight:700}.small{font-size:11px}svg.change-bar{display:block}svg.change-bar .axis{stroke:var(--text-2);stroke-width:1px}svg.change-bar rect{fill:var(--c)}.compare-results .segmented{margin-bottom:.5rem}.detail-pane{gap:.6rem;padding-top:.6rem;display:grid}.entity-group{border:1px solid var(--border);border-left:4px solid var(--c,var(--border));border-radius:6px;padding:.3rem .7rem}.entity-group summary{cursor:pointer;padding:.2rem 0;font-weight:600}.entity-name{width:40%}.entity-name>*{margin-right:.4rem}.finding-cards{gap:.5rem;margin:0 0 .6rem;padding:0;list-style:none;display:grid}.finding-card{border:1px solid var(--border);border-left:4px solid var(--c);border-radius:6px;padding:.5rem .8rem}.finding-card h4{margin:.2rem 0}.finding-card .fix{margin:.3rem 0 0}.sev{color:var(--c);font-weight:700}.trace-note.snapshot{gap:.5rem;width:28rem;display:grid}.trace-note.snapshot p{margin:0}@media (width<=1100px){.sides{grid-template-columns:1fr}.swap{justify-self:center}}.page-map{grid-template-columns:minmax(0,1fr) 20rem;align-items:start;gap:1rem;padding:1rem 1.2rem;display:grid}.page-caption{margin:0 0 .6rem}.page-frame{width:100%;position:relative}.page-frame img{border:1px solid var(--border);border-radius:var(--radius-sm);height:100%;box-shadow:var(--shadow);background:var(--surface);box-sizing:border-box;display:block;position:absolute;top:0}.callout-svg{pointer-events:none;width:100%;height:100%;position:absolute;inset:0}.callout{--pin:var(--heat)}.callout.sev-warn{--pin:var(--warn)}.callout.sev-mild{--pin:var(--accent)}.callout rect{fill:none;stroke:var(--pin);stroke-width:1px;stroke-opacity:.4;vector-effect:non-scaling-stroke}.callout line{stroke:var(--pin);stroke-width:1px;stroke-opacity:.5;vector-effect:non-scaling-stroke}.callout circle{fill:var(--pin);fill-opacity:.8}.callout.active rect{fill:var(--pin);fill-opacity:.16;stroke-opacity:1;stroke-width:2px}.callout.active line{stroke-opacity:1;stroke-width:2px}.callout.active circle{fill-opacity:1}button.marker{--pin:var(--heat);z-index:5;border:2px solid var(--panel-solid);background:var(--pin);color:#fff;border-radius:50%;place-items:center;width:1.7rem;height:1.7rem;min-height:0;padding:0;font-size:11px;font-weight:700;display:grid;position:absolute;transform:translate(-50%,-50%);box-shadow:0 1px 4px #00000059}button.marker.sev-warn{--pin:var(--warn)}button.marker.sev-mild{--pin:var(--accent-strong)}button.marker:hover:not(:disabled),button.marker.active{background:var(--pin);border-color:var(--panel-solid);box-shadow:0 0 0 3px color-mix(in srgb, var(--pin) 40%, transparent);transform:translate(-50%,-50%)scale(1.2)}.pin-num{color:#fff;background:var(--accent-strong);border-radius:999px;place-items:center;min-width:1.4rem;height:1.4rem;padding:0 .3rem;font-size:11px;font-weight:700;display:inline-grid}.pin-card{z-index:20;pointer-events:none;background:color-mix(in srgb, var(--panel-solid) 98%, transparent);border:1px solid var(--panel-border);border-radius:var(--radius-sm);width:20rem;max-width:90%;box-shadow:var(--shadow-pop);color:var(--text);margin:.5rem 0;padding:.8rem .9rem;position:absolute}.pin-card-title{align-items:center;gap:.5rem;margin-bottom:.2rem;display:flex}.pin-file{color:var(--text-muted);overflow-wrap:anywhere;margin-bottom:.5rem;display:block}.pin-stats{grid-template-columns:repeat(4,1fr);gap:.4rem;margin:0 0 .5rem;display:grid}.pin-stats dt{text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);font-size:10px}.pin-stats dd{font-variant-numeric:tabular-nums;margin:0;font-weight:650}.pin-card p{color:var(--text-2);margin:.3rem 0 0}.pin-card .pin-fix{color:var(--text)}.page-list h2{margin:0 0 .5rem;font-size:.9rem}.page-list ol{gap:.3rem;margin:0;padding:0;list-style:none;display:grid}button.hotspot{text-align:left;background:var(--surface-2);white-space:normal;justify-content:flex-start;gap:.6rem;width:100%;padding:.4rem .6rem}button.hotspot.active{border-color:var(--accent);background:var(--accent-soft)}.hotspot-main{flex:1;min-width:0;display:grid}.hotspot-main .mono{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.hotspot-time{font-variant-numeric:tabular-nums;color:var(--text-2);font-weight:600}@media (width<=1100px){.page-map{grid-template-columns:1fr}}.visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.badge-sev{align-items:center;gap:.3rem;font-size:12px;font-weight:600;display:inline-flex}.culprits-hero{grid-template-columns:minmax(18rem,34rem) minmax(0,1fr);align-items:start;gap:1.4rem;display:grid}.hero-sphere h2,.hero-detail h2{margin:0 0 .3rem}.sphere{aspect-ratio:1;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;border-radius:50%;width:100%;max-width:34rem;margin:0 auto;position:relative}.sphere:active{cursor:grabbing}.sphere-glow{background:radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--accent) 26%, transparent), color-mix(in srgb, var(--accent) 6%, transparent) 55%, transparent 72%);box-shadow:inset 0 0 40px color-mix(in srgb, var(--accent) 18%, transparent), 0 0 60px color-mix(in srgb, var(--accent) 14%, transparent);border-radius:50%;position:absolute;inset:12%}.sphere-wire{width:100%;height:100%;position:absolute;inset:0;overflow:visible}.sphere-wire path{fill:none;stroke:var(--accent);stroke-opacity:.22;stroke-width:.006px}button.orb{--orb:var(--accent);background:color-mix(in srgb, var(--panel-solid) 78%, transparent);border:1px solid color-mix(in srgb, var(--orb) 55%, transparent);will-change:transform, opacity;border-radius:999px;justify-items:center;gap:0;min-height:0;padding:.2rem .6rem;line-height:1.2;display:grid;position:absolute;box-shadow:0 2px 10px #0000001f}button.orb.sev-critical{--orb:var(--poor)}button.orb.sev-warning{--orb:var(--warn)}button.orb .orb-name{color:var(--text);font-weight:650}button.orb .orb-ms{color:var(--orb);font-variant-numeric:tabular-nums;font-size:.78em;font-weight:600}button.orb:hover:not(:disabled),button.orb.active{background:color-mix(in srgb, var(--orb) 18%, var(--panel-solid));border-color:var(--orb);box-shadow:0 0 0 3px color-mix(in srgb, var(--orb) 30%, transparent)}.insight{border:1px solid var(--border);border-left:4px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-2);gap:.35rem;padding:.8rem 1rem;display:grid}.insight.sev-critical{border-left-color:var(--poor)}.insight.sev-warning{border-left-color:var(--warn)}.insight.sev-critical .badge-sev{color:var(--poor)}.insight.sev-warning .badge-sev{color:var(--warn)}.insight.sev-info .badge-sev{color:var(--accent)}.insight-head{flex-wrap:wrap;align-items:baseline;gap:.6rem;display:flex}.insight-name{font-family:var(--font-mono);font-size:.95rem}.insight-ms{font-variant-numeric:tabular-nums;margin-left:auto;font-weight:700}.insight p{margin:0}.insight button{justify-self:start}.rank-list{gap:2px;margin:.8rem 0 0;padding:0;list-style:none;display:grid}button.rank-row{--orb:var(--accent);border-radius:var(--radius-sm);text-align:left;background:0 0;border-color:#0000;grid-template-columns:1.4rem minmax(0,1fr) 7rem 4.6rem;align-items:center;gap:.6rem;width:100%;min-height:1.9rem;padding:.15rem .5rem;font-weight:400;display:grid}button.rank-row.sev-critical{--orb:var(--poor)}button.rank-row.sev-warning{--orb:var(--warn)}button.rank-row.active{background:var(--accent-soft);border-color:color-mix(in srgb, var(--accent) 40%, transparent)}.rank-n{color:var(--text-muted);font-variant-numeric:tabular-nums}.rank-name{font-family:var(--font-mono);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.rank-bar{background:var(--grid);border-radius:3px;height:6px;overflow:hidden}.rank-bar span{background:var(--orb);border-radius:3px;height:100%;display:block}.rank-ms{text-align:right;font-variant-numeric:tabular-nums;color:var(--text-2)}@media (width<=1000px){.culprits-hero{grid-template-columns:1fr}}.vitals-block{gap:.7rem;display:grid}.vitals-hero{grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));gap:.7rem;display:grid}.vital-hero{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:4px;gap:.15rem;padding:.8rem 1rem;display:grid}.vital-hero .vital-label{color:var(--text-2)}.vital-hero .vital-value{letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:1.9rem;font-weight:650;line-height:1.15}.vital-hero .vital-rating{font-size:12px;font-weight:600}.vital-target{font-size:11px}.rating-good{--rate:var(--good);border-left-color:var(--good)}.rating-warn{--rate:var(--warn);border-left-color:var(--warn)}.rating-poor{--rate:var(--poor);border-left-color:var(--poor)}.rating-none{--rate:var(--text-muted);border-left-color:var(--border)}.vital-rating{color:var(--rate)}.gauge{background:linear-gradient(90deg, color-mix(in srgb, var(--good) 45%, transparent) 0 var(--g), color-mix(in srgb, var(--warn) 45%, transparent) var(--g) var(--p), color-mix(in srgb, var(--poor) 45%, transparent) var(--p) 100%);border-radius:3px;height:6px;margin:.45rem 0 .2rem;position:relative}.gauge-needle{background:var(--rate);width:4px;height:12px;box-shadow:0 0 0 2px var(--panel-solid);border-radius:2px;margin-left:-2px;position:absolute;top:-3px}.vitals-secondary{grid-template-columns:repeat(auto-fit,minmax(11.5rem,1fr));gap:.4rem;display:grid}.vital-chip{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:1px;align-items:center;gap:.5rem;padding:.35rem .7rem;display:flex}.vital-chip-icon{width:1.1rem;height:1.1rem;color:var(--panel-solid);background:var(--rate);border-radius:50%;place-items:center;font-size:10px;font-weight:700;display:grid}.vital-chip-label{color:var(--text-2);flex:1}.vital-chip b{font-variant-numeric:tabular-nums}.findings-toolbar{flex-wrap:wrap;align-items:center;gap:.5rem;margin-bottom:.6rem;display:flex}.frows,.members{gap:.4rem;margin:0;padding:0;list-style:none;display:grid}.frow{--sev:var(--accent);border:1px solid var(--border);border-left:4px solid var(--sev);border-radius:var(--radius-sm);background:var(--surface-2);overflow:hidden}.frow.sev-critical{--sev:var(--poor)}.frow.sev-warning{--sev:var(--warn)}.frow.open{border-color:color-mix(in srgb, var(--sev) 45%, var(--border))}.frow.group{background:color-mix(in srgb, var(--sev) 6%, var(--surface-2))}button.frow-head{text-align:left;white-space:normal;background:0 0;border:0;border-radius:0;grid-template-columns:1.5rem minmax(0,1fr) auto 9.5rem 1rem;align-items:center;gap:.7rem;width:100%;padding:.55rem .8rem;font-weight:400;display:grid}button.frow-head:hover:not(:disabled){background:var(--control);border:0}.sev-badge{width:1.4rem;height:1.4rem;color:var(--panel-solid);background:var(--sev);border-radius:50%;place-items:center;font-size:11px;font-weight:700;display:grid}.frow-main{min-width:0;display:grid}.frow-title{text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.frow.open>.frow-head .frow-title{white-space:normal}.frow-sub{font-family:var(--font-mono);font-size:11px}.frow-chips{gap:.25rem;display:flex}.impact{grid-template-columns:1fr auto;align-items:center;gap:.5rem;display:grid}.impact-bar{background:var(--grid);border-radius:3px;height:6px;overflow:hidden}.impact-bar span{background:var(--sev);border-radius:3px;height:100%;display:block}.impact-ms{text-align:right;font-variant-numeric:tabular-nums;min-width:3.4rem;font-weight:600}.fbody{gap:.8rem;padding:.2rem 1rem 1rem 3.1rem;display:grid}.fix-callout{border-radius:var(--radius-sm);background:var(--accent-soft);border:1px solid color-mix(in srgb, var(--accent) 35%, transparent);padding:.6rem .9rem}.fix-callout p{margin:.15rem 0 0}.fix-label{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-size:.7rem;font-weight:700}.fdetail{color:var(--text-2);margin:0}.fdetail b{text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);font-size:.7rem;display:block}.culprit-table th{background:0 0;position:static}.culprit-table td{vertical-align:middle}.members-title{text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin:.2rem 0 0;font-size:.7rem}.members .frow{background:var(--surface)}@media (width<=900px){button.frow-head{grid-template-columns:1.5rem minmax(0,1fr) 1rem}.frow-chips,.impact{display:none}.fbody{padding-left:1rem}}.eval-panel{padding:16px 20px 28px;overflow:auto}.eval-head{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:12px;display:flex}.eval-summary{flex-wrap:wrap;gap:6px;display:inline-flex}.eval-target{color:var(--text);margin-bottom:14px;font-size:13px}.eval-table-wrap{border:1px solid var(--border);border-radius:var(--radius-sm);overflow-x:auto}table.eval-matrix{border-collapse:collapse;width:100%;font-size:13px}table.eval-matrix th,table.eval-matrix td{text-align:left;border-bottom:1px solid var(--border);vertical-align:top;padding:9px 12px}table.eval-matrix th{font:inherit;letter-spacing:var(--label-tracking);text-transform:var(--label-case);color:var(--text-muted);background:var(--surface);font-size:11px}table.eval-matrix td.num,table.eval-matrix th.num{text-align:right;font-family:var(--font-mono);white-space:nowrap}table.eval-matrix tr:last-child td{border-bottom:none}table.eval-matrix tr.row-fail td{background:color-mix(in srgb, var(--poor) 8%, transparent)}.eval-path-title{font-weight:600}.eval-path-error{color:var(--poor);font-size:12px;font-family:var(--font-mono);margin-top:3px}.badge{letter-spacing:.04em;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700;display:inline-block}.badge-pass{background:color-mix(in srgb, var(--good) 20%, transparent);color:var(--good)}.badge-fail{background:color-mix(in srgb, var(--poor) 20%, transparent);color:var(--poor)}.top-finding{font-size:12px}.eval-foot{margin-top:12px;font-size:12px}
|
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Performance Profiler</title>
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-COnXcwA_.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DxxJOPn9.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@progen-labs/spectra",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Spectra — a Chrome DevTools Protocol profiler that identifies the files, functions and components causing frontend slowness, across React, Vue, Svelte, Angular and Next.js builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
"@jridgewell/trace-mapping": "^0.3.25",
|
|
68
68
|
"d3": "^7.9.0",
|
|
69
69
|
"highlight.js": "^11.10.0",
|
|
70
|
-
"puppeteer-core": "^
|
|
70
|
+
"puppeteer-core": "^25.13.0",
|
|
71
71
|
"react": "^18.3.1",
|
|
72
72
|
"react-dom": "^18.3.1"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@vitejs/plugin-react": "^
|
|
76
|
-
"vite": "^
|
|
75
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
76
|
+
"vite": "^8.3.4"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -226,9 +226,28 @@ export function buildFindings(report) {
|
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// ----- React
|
|
229
|
+
// ----- Components (React / Vue / …) -------------------------------------
|
|
230
230
|
const componentCulprit = (c, ms, note) =>
|
|
231
231
|
culprit(c.name, c.file, c.line, c.lineKind, c.abs, ms, note);
|
|
232
|
+
const fw = react.framework || 'react';
|
|
233
|
+
const CAT =
|
|
234
|
+
fw === 'vue'
|
|
235
|
+
? 'Vue'
|
|
236
|
+
: fw === 'svelte'
|
|
237
|
+
? 'Svelte'
|
|
238
|
+
: fw === 'angular'
|
|
239
|
+
? 'Angular'
|
|
240
|
+
: 'React';
|
|
241
|
+
const slowFix =
|
|
242
|
+
fw === 'vue'
|
|
243
|
+
? 'Move expensive work into a computed property, use v-memo/v-once for static subtrees, and virtualise long lists.'
|
|
244
|
+
: fw === 'react' || fw === 'next'
|
|
245
|
+
? 'Move expensive calculations into useMemo, virtualise long lists, and avoid deriving large data structures inside render.'
|
|
246
|
+
: 'Memoize expensive work, virtualise long lists, and avoid building large data structures during render.';
|
|
247
|
+
const frequentFix =
|
|
248
|
+
fw === 'vue'
|
|
249
|
+
? 'Find the reactive dependency that changes this often (a ref/store/timer), narrow it, and use v-memo to skip unchanged subtrees.'
|
|
250
|
+
: 'Find what updates this often (a timer, a store subscription, a scroll handler) and throttle it or narrow the selector/subscription.';
|
|
232
251
|
if (react.detected && react.hasTimings) {
|
|
233
252
|
react.components
|
|
234
253
|
.filter(c => c.wasted >= 5 && c.wasted / c.renders >= 0.3)
|
|
@@ -240,7 +259,7 @@ export function buildFindings(report) {
|
|
|
240
259
|
id: `wasted-${c.name}-${c.file}`,
|
|
241
260
|
severity:
|
|
242
261
|
c.wastedMs >= 30 || c.wasted >= 100 ? 'critical' : 'warning',
|
|
243
|
-
category:
|
|
262
|
+
category: CAT,
|
|
244
263
|
title: `${c.name} re-rendered ${c.renders}× — ${c.wasted} with nothing changed (${Math.round((c.wasted / c.renders) * 100)}%)`,
|
|
245
264
|
detail:
|
|
246
265
|
`${where(c.file, c.line)}. ${fmt(c.wastedMs)} spent on renders whose props, state and context were identical.` +
|
|
@@ -272,7 +291,7 @@ export function buildFindings(report) {
|
|
|
272
291
|
add({
|
|
273
292
|
id: `unstable-${c.name}-${p.prop}`,
|
|
274
293
|
severity: 'warning',
|
|
275
|
-
category:
|
|
294
|
+
category: CAT,
|
|
276
295
|
title: `${c.name} gets a new ${p.kind === 'function' ? 'function' : 'object'} for “${p.prop}” on every render (${p.count}×)`,
|
|
277
296
|
detail: `${where(c.file, c.line)}. The value is recreated with ${p.kind === 'function' ? 'identical code' : 'identical contents'} but a new identity, which defeats React.memo and re-triggers effects that depend on it. Look at where ${c.topParent || 'the parent'} passes “${p.prop}”.`,
|
|
278
297
|
file: c.file,
|
|
@@ -300,13 +319,13 @@ export function buildFindings(report) {
|
|
|
300
319
|
add({
|
|
301
320
|
id: `slow-component-${c.name}-${c.file}`,
|
|
302
321
|
severity: c.selfMs >= 300 ? 'critical' : 'warning',
|
|
303
|
-
category:
|
|
322
|
+
category: CAT,
|
|
304
323
|
title: `${c.name} is slow to render: ${fmt(c.selfMs)} total, ${fmt(c.selfMs / c.renders)} per render (${c.renders}×)`,
|
|
305
324
|
detail: `${where(c.file, c.line)}. Self time excludes its children.`,
|
|
306
325
|
file: c.file,
|
|
307
326
|
culprits: [componentCulprit(c, c.selfMs, 'slow render')],
|
|
308
327
|
impactMs: c.selfMs,
|
|
309
|
-
fix:
|
|
328
|
+
fix: slowFix,
|
|
310
329
|
});
|
|
311
330
|
});
|
|
312
331
|
react.components
|
|
@@ -321,7 +340,7 @@ export function buildFindings(report) {
|
|
|
321
340
|
add({
|
|
322
341
|
id: `frequent-${c.name}-${c.file}`,
|
|
323
342
|
severity: 'warning',
|
|
324
|
-
category:
|
|
343
|
+
category: CAT,
|
|
325
344
|
title: `${c.name} rendered ${c.renders}× (${(c.renders / (report.meta.durationMs / 1000)).toFixed(0)}/s)`,
|
|
326
345
|
detail: `${where(c.file, c.line)}. Reasons: ${
|
|
327
346
|
Object.entries(c.reasons)
|
|
@@ -332,7 +351,7 @@ export function buildFindings(report) {
|
|
|
332
351
|
file: c.file,
|
|
333
352
|
culprits: [componentCulprit(c, c.selfMs, 'renders very often')],
|
|
334
353
|
impactMs: c.selfMs,
|
|
335
|
-
fix:
|
|
354
|
+
fix: frequentFix,
|
|
336
355
|
});
|
|
337
356
|
});
|
|
338
357
|
react.slowCommits.slice(0, 3).forEach((c, i) => {
|
|
@@ -340,8 +359,8 @@ export function buildFindings(report) {
|
|
|
340
359
|
add({
|
|
341
360
|
id: `commit-${i}`,
|
|
342
361
|
severity: c.dur >= 100 ? 'critical' : 'warning',
|
|
343
|
-
category:
|
|
344
|
-
title:
|
|
362
|
+
category: CAT,
|
|
363
|
+
title: `${CAT} commit took ${fmt(c.dur)} (${c.rendered} components rendered, ${c.wasted} wasted)`,
|
|
345
364
|
detail: `At ${(c.tMs / 1000).toFixed(2)}s.`,
|
|
346
365
|
culprits: react.components
|
|
347
366
|
.slice(0, 3)
|
|
@@ -356,20 +375,20 @@ export function buildFindings(report) {
|
|
|
356
375
|
add({
|
|
357
376
|
id: 'react-prod',
|
|
358
377
|
severity: 'info',
|
|
359
|
-
category:
|
|
360
|
-
title:
|
|
378
|
+
category: CAT,
|
|
379
|
+
title: `${CAT} is a production build: render timings are unavailable`,
|
|
361
380
|
detail:
|
|
362
381
|
'Render counts and reasons are still collected where possible, but durations need a development or profiling build.',
|
|
363
|
-
fix: 'Profile against the dev server (npm run
|
|
382
|
+
fix: 'Profile against the dev server (npm run dev / start).',
|
|
364
383
|
});
|
|
365
384
|
} else if (!react.detected) {
|
|
366
385
|
add({
|
|
367
386
|
id: 'react-missing',
|
|
368
387
|
severity: 'info',
|
|
369
|
-
category:
|
|
370
|
-
title:
|
|
388
|
+
category: CAT,
|
|
389
|
+
title: `No ${CAT === 'React' ? 'React renderer' : CAT + ' app'} was seen on this page`,
|
|
371
390
|
detail:
|
|
372
|
-
'The probe must be installed before
|
|
391
|
+
'The probe must be installed before the framework loads (use “Reload & record”), and component-level timings need a development build.',
|
|
373
392
|
fix: 'Click “Record page load”, or reload the tab from this tool.',
|
|
374
393
|
});
|
|
375
394
|
}
|
|
@@ -19,8 +19,21 @@ import {
|
|
|
19
19
|
findMainThread,
|
|
20
20
|
} from './trace.js';
|
|
21
21
|
import { packageOf } from './sources.js';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { FRONTEND_ROOT } from '../config.js';
|
|
22
24
|
|
|
23
25
|
const round = (n, d = 2) => Math.round(n * 10 ** d) / 10 ** d;
|
|
26
|
+
|
|
27
|
+
/** Turn an absolute component file (e.g. Vue SFC __file) into a project path. */
|
|
28
|
+
function toProjectRelative(abs) {
|
|
29
|
+
try {
|
|
30
|
+
if (!abs || !path.isAbsolute(abs)) return abs || '';
|
|
31
|
+
const rel = path.relative(FRONTEND_ROOT, abs);
|
|
32
|
+
return rel.startsWith('..') ? abs : rel;
|
|
33
|
+
} catch {
|
|
34
|
+
return abs || '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
24
37
|
const ms = us => us / 1000;
|
|
25
38
|
|
|
26
39
|
function newFile(path, vendor, pkg) {
|
|
@@ -635,6 +648,7 @@ function summarizeReact(
|
|
|
635
648
|
) {
|
|
636
649
|
const empty = {
|
|
637
650
|
detected: false,
|
|
651
|
+
framework: probe?.info?.framework || '',
|
|
638
652
|
hasTimings: false,
|
|
639
653
|
version: '',
|
|
640
654
|
components: [],
|
|
@@ -642,12 +656,22 @@ function summarizeReact(
|
|
|
642
656
|
slowCommits: [],
|
|
643
657
|
commitSeries: [],
|
|
644
658
|
};
|
|
645
|
-
if (!probe || !probe.info?.
|
|
659
|
+
if (!probe || !probe.info?.detected) return empty;
|
|
646
660
|
const originMs = probe.since || 0;
|
|
647
661
|
const components = probe.components
|
|
648
662
|
.map(c => {
|
|
649
663
|
let loc;
|
|
650
|
-
if (
|
|
664
|
+
if (c.file) {
|
|
665
|
+
// Adapters that know the component's source file directly (e.g. Vue SFCs).
|
|
666
|
+
const rel = toProjectRelative(c.file);
|
|
667
|
+
loc = {
|
|
668
|
+
file: rel,
|
|
669
|
+
line: c.line || null,
|
|
670
|
+
vendor: /(^|\/)node_modules\//.test(rel),
|
|
671
|
+
pkg: null,
|
|
672
|
+
exact: false,
|
|
673
|
+
};
|
|
674
|
+
} else if (locations.has(c.id)) {
|
|
651
675
|
const l = locations.get(c.id);
|
|
652
676
|
const script = resolver.scripts.get(String(l.scriptId));
|
|
653
677
|
loc = resolver.resolve(
|
|
@@ -722,6 +746,7 @@ function summarizeReact(
|
|
|
722
746
|
const sorted = [...commits].sort((a, b) => b.dur - a.dur);
|
|
723
747
|
return {
|
|
724
748
|
detected: true,
|
|
749
|
+
framework: probe.info.framework || 'react',
|
|
725
750
|
hasTimings: probe.info.timings,
|
|
726
751
|
version: probe.info.version,
|
|
727
752
|
components: components.slice(0, 600),
|
package/server/chrome.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function launchChrome({ headless = false } = {}) {
|
|
|
45
45
|
fs.mkdirSync(CHROME_PROFILE_DIR, { recursive: true });
|
|
46
46
|
const options = {
|
|
47
47
|
executablePath,
|
|
48
|
-
headless: headless
|
|
48
|
+
headless: headless, // true = modern headless in puppeteer-core 23+
|
|
49
49
|
userDataDir: CHROME_PROFILE_DIR,
|
|
50
50
|
defaultViewport: null,
|
|
51
51
|
ignoreDefaultArgs: ['--enable-automation'],
|
package/server/probe.browser.js
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
if (window.__PERF_PROBE__) return;
|
|
16
16
|
// Runtime hint injected by the profiler: { framework, bundler, isSSR }
|
|
17
17
|
var SPECTRA_FRAMEWORK = typeof window.__SPECTRA_FRAMEWORK__ !== 'undefined' ? window.__SPECTRA_FRAMEWORK__ : null;
|
|
18
|
-
var WANT_REACT =
|
|
18
|
+
var WANT_REACT =
|
|
19
|
+
!SPECTRA_FRAMEWORK ||
|
|
20
|
+
SPECTRA_FRAMEWORK.framework === 'react' ||
|
|
21
|
+
SPECTRA_FRAMEWORK.framework === 'next' ||
|
|
22
|
+
SPECTRA_FRAMEWORK.framework === 'unknown';
|
|
19
23
|
|
|
20
24
|
var PERFORMED_WORK = 1;
|
|
21
25
|
var TAG_FUNCTION = 0;
|
|
@@ -521,9 +525,14 @@
|
|
|
521
525
|
};
|
|
522
526
|
|
|
523
527
|
probe.info = function () {
|
|
528
|
+
var fw =
|
|
529
|
+
probe.frameworkName ||
|
|
530
|
+
(probe.renderers > 0 ? 'react' : (SPECTRA_FRAMEWORK && SPECTRA_FRAMEWORK.framework) || '');
|
|
524
531
|
return {
|
|
525
532
|
react: probe.renderers > 0,
|
|
526
|
-
|
|
533
|
+
framework: fw,
|
|
534
|
+
detected: probe.renderers > 0 || !!probe.frameworkDetected,
|
|
535
|
+
version: probe.reactVersion || probe.frameworkVersion || '',
|
|
527
536
|
active: probe.active,
|
|
528
537
|
timings: probe.sawTimings,
|
|
529
538
|
};
|
|
@@ -584,6 +593,134 @@
|
|
|
584
593
|
// Fail silently: probe should still collect vitals even if hooking fails.
|
|
585
594
|
}
|
|
586
595
|
|
|
596
|
+
// ------------------------------------------------ Vue adapter
|
|
597
|
+
// Uses Vue's devtools global hook (dev builds). component:added / :updated
|
|
598
|
+
// give per-component mount/update counts and names, and perf:start / perf:end
|
|
599
|
+
// give real per-component render time. Component source files come straight
|
|
600
|
+
// from the SFC compiler (type.__file), so no source-map step is needed.
|
|
601
|
+
function installVueAdapter() {
|
|
602
|
+
var vueIds = new Map();
|
|
603
|
+
var nextId = 0;
|
|
604
|
+
var perfStart = {};
|
|
605
|
+
|
|
606
|
+
function getStat(instance) {
|
|
607
|
+
if (!instance || !instance.type) return null;
|
|
608
|
+
var type = instance.type;
|
|
609
|
+
var id = vueIds.get(type);
|
|
610
|
+
if (id === undefined) {
|
|
611
|
+
id = nextId++;
|
|
612
|
+
vueIds.set(type, id);
|
|
613
|
+
}
|
|
614
|
+
var stat = probe.stats[id];
|
|
615
|
+
if (!stat) {
|
|
616
|
+
stat = probe.stats[id] = {
|
|
617
|
+
id: id,
|
|
618
|
+
name: type.__name || type.name || type.__file && baseName(type.__file) || 'Anonymous',
|
|
619
|
+
kind: 'vue',
|
|
620
|
+
renders: 0,
|
|
621
|
+
mounts: 0,
|
|
622
|
+
wasted: 0,
|
|
623
|
+
selfMs: 0,
|
|
624
|
+
totalMs: 0,
|
|
625
|
+
maxMs: 0,
|
|
626
|
+
reasons: { props: 0, state: 0, hooks: 0, context: 0, parent: 0 },
|
|
627
|
+
props: {},
|
|
628
|
+
parents: {},
|
|
629
|
+
file: type.__file || '',
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return stat;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function baseName(p) {
|
|
636
|
+
var parts = String(p).split(/[\\/]/);
|
|
637
|
+
return parts[parts.length - 1].replace(/\.\w+$/, '');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function recordParent(stat, instance) {
|
|
641
|
+
var p = instance && instance.parent;
|
|
642
|
+
var name = p && p.type && (p.type.__name || p.type.name);
|
|
643
|
+
if (name) stat.parents[name] = (stat.parents[name] || 0) + 1;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function bumpCommit(dur) {
|
|
647
|
+
probe.commits.push({ t: performance.now(), dur: dur || 0, rendered: 1, wasted: 0 });
|
|
648
|
+
probe.commitCount = (probe.commitCount || 0) + 1;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function handle(evt, args) {
|
|
652
|
+
if (evt === 'app:init') {
|
|
653
|
+
probe.frameworkName = 'vue';
|
|
654
|
+
probe.frameworkDetected = true;
|
|
655
|
+
probe.frameworkVersion = args[1] || '';
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!probe.active) return;
|
|
659
|
+
if (evt === 'component:added') {
|
|
660
|
+
var stat = getStat(args[3]);
|
|
661
|
+
if (stat) {
|
|
662
|
+
stat.mounts++;
|
|
663
|
+
stat.renders++;
|
|
664
|
+
recordParent(stat, args[3]);
|
|
665
|
+
bumpCommit();
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (evt === 'component:updated') {
|
|
670
|
+
var s2 = getStat(args[3]);
|
|
671
|
+
if (s2) {
|
|
672
|
+
s2.renders++;
|
|
673
|
+
recordParent(s2, args[3]);
|
|
674
|
+
bumpCommit();
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (evt === 'perf:start' || evt === 'perf:end') {
|
|
679
|
+
var uid = args[1];
|
|
680
|
+
var inst = args[2];
|
|
681
|
+
var phase = args[3];
|
|
682
|
+
var time = args[4];
|
|
683
|
+
var key = uid + ':' + phase;
|
|
684
|
+
if (evt === 'perf:start') {
|
|
685
|
+
perfStart[key] = time;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
var start = perfStart[key];
|
|
689
|
+
if (start === undefined) return;
|
|
690
|
+
delete perfStart[key];
|
|
691
|
+
var dur = Math.max(0, time - start);
|
|
692
|
+
var st = getStat(inst);
|
|
693
|
+
if (!st) return;
|
|
694
|
+
if (phase === 'render') {
|
|
695
|
+
st.selfMs += dur;
|
|
696
|
+
st.totalMs += dur;
|
|
697
|
+
if (dur > st.maxMs) st.maxMs = dur;
|
|
698
|
+
probe.sawTimings = true;
|
|
699
|
+
} else if (phase === 'patch') {
|
|
700
|
+
st.totalMs += dur;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
var existing = window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
706
|
+
var hook = existing || {};
|
|
707
|
+
hook.enabled = true;
|
|
708
|
+
hook.apps = hook.apps || [];
|
|
709
|
+
hook.store = hook.store || {};
|
|
710
|
+
var prevEmit = typeof hook.emit === 'function' ? hook.emit.bind(hook) : null;
|
|
711
|
+
hook.on = hook.on || function () {};
|
|
712
|
+
hook.once = hook.once || function () {};
|
|
713
|
+
hook.off = hook.off || function () {};
|
|
714
|
+
hook.emit = function (evt) {
|
|
715
|
+
try {
|
|
716
|
+
handle(evt, Array.prototype.slice.call(arguments, 1));
|
|
717
|
+
} catch (e) {}
|
|
718
|
+
if (prevEmit) try { prevEmit.apply(null, arguments); } catch (e) {}
|
|
719
|
+
};
|
|
720
|
+
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
|
721
|
+
probe.frameworkName = 'vue';
|
|
722
|
+
}
|
|
723
|
+
|
|
587
724
|
// ------------------------------------------------ Framework fallbacks
|
|
588
725
|
// Provide lightweight probes for Vue, Svelte and Angular when React is not detected.
|
|
589
726
|
function installFrameworkProbes() {
|
|
@@ -683,7 +820,9 @@
|
|
|
683
820
|
}
|
|
684
821
|
|
|
685
822
|
try {
|
|
686
|
-
|
|
823
|
+
var fwHint = SPECTRA_FRAMEWORK && SPECTRA_FRAMEWORK.framework;
|
|
824
|
+
if (fwHint === 'vue') installVueAdapter();
|
|
825
|
+
else if (!WANT_REACT) installFrameworkProbes(); // svelte/angular: heuristic for now
|
|
687
826
|
} catch (e) {}
|
|
688
827
|
installObservers();
|
|
689
828
|
})();
|
package/src/App.jsx
CHANGED
|
@@ -23,7 +23,7 @@ const TABS = [
|
|
|
23
23
|
{ id: 'page', label: 'Page map' },
|
|
24
24
|
{ id: 'files', label: 'File tree' },
|
|
25
25
|
{ id: 'calls', label: 'Call tree' },
|
|
26
|
-
{ id: 'react', label: '
|
|
26
|
+
{ id: 'react', label: 'Components' },
|
|
27
27
|
{ id: 'tasks', label: 'Long tasks' },
|
|
28
28
|
{ id: 'network', label: 'Network & memory' },
|
|
29
29
|
{ id: 'evaluation', label: 'Golden paths' },
|
|
@@ -101,14 +101,18 @@ export default function ReactPanel({ report, hideVendor, onOpenFile }) {
|
|
|
101
101
|
);
|
|
102
102
|
}, [react.components, sort, hideVendor]);
|
|
103
103
|
|
|
104
|
+
const fwLabel =
|
|
105
|
+
{ vue: 'Vue', svelte: 'Svelte', angular: 'Angular', next: 'React' }[
|
|
106
|
+
react.framework
|
|
107
|
+
] || 'React';
|
|
104
108
|
if (!react.detected) {
|
|
105
109
|
return (
|
|
106
110
|
<div className="empty">
|
|
107
|
-
<h3>No
|
|
111
|
+
<h3>No component framework was detected</h3>
|
|
108
112
|
<p>
|
|
109
|
-
The probe has to be installed before
|
|
113
|
+
The probe has to be installed before the framework loads. Use{' '}
|
|
110
114
|
<b>Record page load</b>, or reload the tab from this tool and record
|
|
111
|
-
again.
|
|
115
|
+
again. Component timings need a development build.
|
|
112
116
|
</p>
|
|
113
117
|
</div>
|
|
114
118
|
);
|
|
@@ -116,7 +120,9 @@ export default function ReactPanel({ report, hideVendor, onOpenFile }) {
|
|
|
116
120
|
return (
|
|
117
121
|
<div className="react-panel">
|
|
118
122
|
<div className="summary-row">
|
|
119
|
-
<span>
|
|
123
|
+
<span>
|
|
124
|
+
{fwLabel} {react.version}
|
|
125
|
+
</span>
|
|
120
126
|
<span>{fmtNumber(react.commits.count)} commits</span>
|
|
121
127
|
<span>total render time {fmtMs(react.commits.totalMs)}</span>
|
|
122
128
|
<span>slowest commit {fmtMs(react.commits.maxMs)}</span>
|