@progen-labs/spectra 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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}.signin{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-2);gap:.6rem;padding:.6rem .7rem;display:grid}.signin.on{border-color:color-mix(in srgb, var(--accent) 45%, var(--border))}.signin>.switch{align-items:flex-start}.signin>.switch b{color:var(--text);font-weight:600}.signin-body{gap:.55rem;display:grid}.signin-methods{flex-wrap:wrap;gap:.35rem;display:flex}.signin-method{min-height:1.8rem;color:var(--text-2);border-radius:999px;padding:.2rem .75rem}.signin-method.selected,.signin-method.selected:hover{background:var(--accent-soft);border-color:var(--accent);color:var(--accent);font-weight:600}.signin-hint{margin:0}.signin-grid{grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));align-items:end;gap:.5rem .6rem;display:grid}.signin-grid .wide{grid-column:1/-1}.signin-grid p{margin:.2rem 0 0}.signin-more{justify-items:start;gap:.5rem;display:grid}.signin-more>.signin-grid{width:100%}button.small{min-height:1.6rem;padding:.1rem .5rem;font-size:.75rem}.secret-tag{border:1px solid var(--border);color:var(--text-muted);vertical-align:1px;border-radius:999px;margin-left:.4rem;padding:0 .4rem;font-size:.65rem}.signin-note{color:var(--text-muted);margin:0;font-size:.72rem}.signin-note b{color:var(--warn,var(--text))}.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);flex-wrap:wrap;align-items:center;row-gap:.4rem;padding:.5rem .9rem;overflow-x:visible}.main-tabs .tab-list{flex-wrap:wrap;gap:.2rem;min-width:0;display:flex}.main-tabs label.switch{white-space:nowrap;margin-left: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:minmax(0,1fr) minmax(0,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,button.link.row{text-align:left;justify-content:space-between;gap:.6rem;width:100%;display:flex}.row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.row b{white-space:nowrap;flex:none}.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-map.no-pins{grid-template-columns:minmax(0,1fr)}.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-DY8VmG9y.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Dx1YSFIx.css">
8
+ <script type="module" crossorigin src="/assets/index-mtYxDBW7.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-z14kjW8r.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@progen-labs/spectra",
3
- "version": "0.4.1",
4
- "description": "Spectra \u2014 a Chrome DevTools Protocol profiler that identifies the files, functions and components causing frontend slowness, across React, Vue, Svelte, Angular and Next.js builds.",
3
+ "version": "0.6.0",
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": {
7
7
  "spectra": "bin/spectra.js"
@@ -18,6 +18,7 @@
18
18
  "server",
19
19
  "shared",
20
20
  "schema",
21
+ "agents",
21
22
  "src",
22
23
  "dist",
23
24
  "index.html",
@@ -34,7 +35,8 @@
34
35
  "profile": "node server/cli.js",
35
36
  "test": "node --test \"server/**/test-*.js\" \"action/test-*.js\"",
36
37
  "test:vscode": "npm --prefix vscode test",
37
- "bench": "node bench/run.mjs"
38
+ "bench": "node bench/run.mjs",
39
+ "postinstall": "node server/postinstall.js"
38
40
  },
39
41
  "engines": {
40
42
  "node": ">=18"
@@ -89,6 +89,80 @@
89
89
  "description": "All findings"
90
90
  }
91
91
  }
92
+ },
93
+ "signIn": {
94
+ "type": "object",
95
+ "description": "How Spectra signs in to your app before profiling it. Only the non-secret parts: the password, the authenticator secret, header values, cookies and storage entries come from the environment (SPECTRA_LOGIN_PASSWORD, SPECTRA_LOGIN_TOTP_SECRET, SPECTRA_LOGIN_HEADER, SPECTRA_LOGIN_COOKIES, SPECTRA_LOGIN_STORAGE).",
96
+ "additionalProperties": false,
97
+ "properties": {
98
+ "method": {
99
+ "enum": [
100
+ "form",
101
+ "link",
102
+ "http",
103
+ "token",
104
+ "manual"
105
+ ],
106
+ "description": "form: a login form; link: a URL that signs in by itself; http: the browser's login prompt; token: a header, cookies or localStorage entries; manual: you sign in in the Chrome window"
107
+ },
108
+ "url": {
109
+ "type": "string",
110
+ "description": "The login page, or the sign-in link. {url} in it becomes the page to return to"
111
+ },
112
+ "username": {
113
+ "type": "string",
114
+ "description": "The username or email"
115
+ },
116
+ "startSelector": {
117
+ "type": "string",
118
+ "description": "CSS selector for a control to click first, such as a button that opens the login, when Spectra can't find it"
119
+ },
120
+ "usernameSelector": {
121
+ "type": "string",
122
+ "description": "CSS selector for the username field, when Spectra can't find it"
123
+ },
124
+ "passwordSelector": {
125
+ "type": "string",
126
+ "description": "CSS selector for the password field, when Spectra can't find it"
127
+ },
128
+ "submitSelector": {
129
+ "type": "string",
130
+ "description": "CSS selector for the submit button, when Spectra can't find it"
131
+ },
132
+ "codeSelector": {
133
+ "type": "string",
134
+ "description": "CSS selector for the one-time code field, when Spectra can't find it"
135
+ },
136
+ "waitSeconds": {
137
+ "type": "number",
138
+ "exclusiveMinimum": 0,
139
+ "description": "How long to wait for a manual sign-in (default 300)"
140
+ }
141
+ }
142
+ },
143
+ "agentDocs": {
144
+ "description": "Spectra's knowledge doc for AI agents, written after npm install (see `spectra agents`). false: never write it; a list: only for these agents. Left out: AGENTS.md and the agents this project or machine uses.",
145
+ "oneOf": [
146
+ {
147
+ "type": "boolean"
148
+ },
149
+ {
150
+ "type": "array",
151
+ "items": {
152
+ "enum": [
153
+ "agents-md",
154
+ "claude",
155
+ "cursor",
156
+ "copilot",
157
+ "windsurf",
158
+ "cline",
159
+ "roo",
160
+ "gemini"
161
+ ]
162
+ },
163
+ "uniqueItems": true
164
+ }
165
+ ]
92
166
  }
93
167
  }
94
168
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * spectra agents: puts Spectra's knowledge doc where AI agents read their
3
+ * project instructions (server/agents.js), or takes it out again.
4
+ *
5
+ * spectra agents the agents this project or machine uses, and AGENTS.md
6
+ * spectra agents --all every supported agent
7
+ * spectra agents --only claude,copilot
8
+ * spectra agents --remove take it out of every agent's files
9
+ * spectra agents --list the supported agents, and which ones were detected
10
+ * spectra agents --print print the knowledge doc
11
+ * … --dir <project> --dry-run
12
+ */
13
+ import path from 'node:path';
14
+ import { parseArgs } from './args.js';
15
+ import { TARGETS, TARGET_IDS, describe, detectTargets, installAgentDocs, knowledge, removeAgentDocs } from './agents.js';
16
+
17
+ const { opt, flag } = parseArgs(process.argv.slice(2), { values: ['dir', 'only'] });
18
+ const root = path.resolve(opt('dir', process.cwd()));
19
+ const dryRun = flag('dry-run');
20
+
21
+ if (flag('help') || process.argv.includes('-h')) {
22
+ console.log(`usage: spectra agents [--all | --only <ids>] [--remove] [--list] [--print] [--dir <project>] [--dry-run]
23
+
24
+ Teaches your AI agents about Spectra: what it does, which command answers which
25
+ request, and how to read the results. It writes Spectra's knowledge doc where
26
+ each agent reads its project instructions, so you can ask in plain words
27
+ ("why is checkout slow?", "did my change help?") and the agent runs Spectra.
28
+ It runs by itself after npm install; this does it by hand.
29
+
30
+ Agents: ${TARGET_IDS.join(', ')}
31
+ Off for a project: "agentDocs": false in spectra.config.json (or SPECTRA_AGENT_DOCS=0).`);
32
+ process.exit(0);
33
+ }
34
+
35
+ if (flag('print')) {
36
+ console.log(knowledge());
37
+ process.exit(0);
38
+ }
39
+
40
+ if (flag('list')) {
41
+ const found = new Set(detectTargets({ root }));
42
+ for (const t of TARGETS) console.log(` ${found.has(t.id) ? '✓' : ' '} ${t.id.padEnd(10)} ${t.file.padEnd(46)} ${t.name}`);
43
+ console.log('\n✓ = written by default (detected in this project or on this machine). --all writes every one.');
44
+ process.exit(0);
45
+ }
46
+
47
+ if (flag('remove')) {
48
+ const results = removeAgentDocs({ root, dryRun });
49
+ console.log(results.length ? `${dryRun ? 'Would remove' : 'Removed'} Spectra's knowledge doc:\n${describe(results)}` : 'Nothing to remove.');
50
+ process.exit(0);
51
+ }
52
+
53
+ let targets;
54
+ if (flag('all')) targets = TARGET_IDS;
55
+ else if (opt('only')) {
56
+ targets = String(opt('only')).split(',').map(s => s.trim()).filter(Boolean);
57
+ const unknown = targets.filter(id => !TARGET_IDS.includes(id));
58
+ if (unknown.length) {
59
+ console.error(`spectra: unknown agent ${unknown.join(', ')} (known: ${TARGET_IDS.join(', ')})`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+ const results = installAgentDocs({ root, targets, dryRun });
64
+ console.log(`${dryRun ? 'Would write' : 'Wrote'} Spectra's knowledge doc for your AI agents:\n${describe(results)}`);
65
+ console.log('\nAsk your agent in plain words, e.g. "why is the checkout page slow?" or "did my last change make it faster?".');
66
+ console.log('Remove it with: npx spectra agents --remove');
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Teaches AI coding agents about Spectra. The knowledge doc (agents/spectra.md:
3
+ * what Spectra does, which command answers which request, how to read the
4
+ * results, the rules) is written where each agent reads its project
5
+ * instructions, so a user can just ask in plain words ("why is checkout
6
+ * slow?", "did my change help?") and the agent runs the right Spectra command.
7
+ *
8
+ * Runs after `npm install @progen-labs/spectra` (server/postinstall.js) for the
9
+ * agents this machine or project uses, and on demand with `spectra agents`.
10
+ *
11
+ * Files Spectra owns carry a `spectra:managed` marker and are replaced on
12
+ * update; in shared files (AGENTS.md, CLAUDE.md, GEMINI.md) only the block
13
+ * between the `spectra:start` and `spectra:end` markers is touched. A file
14
+ * without the marker is never overwritten, and `--remove` takes everything
15
+ * back out, leaving the rest of each file as it was.
16
+ */
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ export const DOC_FILE = path.join(PKG_ROOT, 'agents', 'spectra.md');
24
+ const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
25
+
26
+ const MANAGED = 'spectra:managed';
27
+ const START = '<!-- spectra:start -->';
28
+ const END = '<!-- spectra:end -->';
29
+ const note = `written by @progen-labs/spectra ${VERSION} and replaced when it updates; remove with: npx spectra agents --remove`;
30
+ const DESCRIPTION =
31
+ 'Spectra (@progen-labs/spectra, installed in this project) measures and improves frontend performance. Use it when the user asks why the app or a page is slow, about jank, re-renders, long tasks, load time or Web Vitals (LCP, TBT, CLS, INP), to profile a page or an interaction, to prove that a change made things faster, to set performance budgets, or to profile pages behind a login.';
32
+
33
+ /** The knowledge doc, with its headings one level deeper when it sits inside another file. */
34
+ export function knowledge({ nested = false } = {}) {
35
+ const doc = fs.readFileSync(DOC_FILE, 'utf8').trim();
36
+ return nested ? doc.replace(/^(#{1,5}) /gm, '#$1 ') : doc;
37
+ }
38
+
39
+ const frontmatter = fields =>
40
+ `---\n${Object.entries(fields)
41
+ .map(([k, v]) => (v === '' ? `${k}:` : `${k}: ${v}`))
42
+ .join('\n')}\n---\n`;
43
+ const ownFile = (head = '') => `${head}<!-- ${MANAGED}: ${note} -->\n\n${knowledge()}\n`;
44
+ const quoted = s => JSON.stringify(s);
45
+
46
+ /**
47
+ * Where each agent reads project instructions. `home` and `project` are the
48
+ * signs that the agent is in use: a folder or file in the user's home, or in
49
+ * the project. AGENTS.md, the cross-agent standard, is always written.
50
+ */
51
+ export const TARGETS = [
52
+ {
53
+ id: 'agents-md',
54
+ name: 'AGENTS.md (Codex, GitHub Copilot, Cursor, Windsurf, Zed, Jules, Amp and other agents)',
55
+ file: 'AGENTS.md',
56
+ block: () => knowledge({ nested: true }),
57
+ always: true,
58
+ },
59
+ {
60
+ id: 'claude',
61
+ name: 'Claude Code',
62
+ file: '.claude/skills/spectra/SKILL.md',
63
+ content: () => ownFile(frontmatter({ name: 'spectra', description: quoted(DESCRIPTION) })),
64
+ // and a short note in its memory file, which it always reads
65
+ also: {
66
+ file: 'CLAUDE.md',
67
+ block: () =>
68
+ '## Spectra (performance)\n\nThis project has Spectra (`@progen-labs/spectra`) installed. For anything about speed, slowness, re-renders, Web Vitals or proving that a change made things faster, follow the `spectra` skill (`.claude/skills/spectra/SKILL.md`) and measure with `npx spectra …` instead of guessing.',
69
+ },
70
+ project: ['CLAUDE.md', '.claude'],
71
+ home: ['.claude', '.claude.json'],
72
+ },
73
+ {
74
+ id: 'cursor',
75
+ name: 'Cursor',
76
+ file: '.cursor/rules/spectra.mdc',
77
+ content: () => ownFile(frontmatter({ description: quoted(DESCRIPTION), globs: '', alwaysApply: 'false' })),
78
+ project: ['.cursor', '.cursorrules'],
79
+ home: ['.cursor'],
80
+ },
81
+ {
82
+ id: 'copilot',
83
+ name: 'GitHub Copilot (VS Code, Visual Studio, JetBrains, the coding agent)',
84
+ file: '.github/instructions/spectra.instructions.md',
85
+ content: () => ownFile(frontmatter({ applyTo: quoted('**'), description: quoted(DESCRIPTION) })),
86
+ project: ['.github/copilot-instructions.md', '.github/instructions', '.github/prompts', '.vscode'],
87
+ home: ['.vscode', '.vscode-insiders', '.config/github-copilot'],
88
+ },
89
+ {
90
+ id: 'windsurf',
91
+ name: 'Windsurf',
92
+ file: '.windsurf/rules/spectra.md',
93
+ content: () => ownFile(frontmatter({ trigger: 'model_decision', description: quoted(DESCRIPTION) })),
94
+ project: ['.windsurf', '.windsurfrules'],
95
+ home: ['.codeium/windsurf'],
96
+ },
97
+ {
98
+ id: 'cline',
99
+ name: 'Cline',
100
+ file: '.clinerules/spectra.md',
101
+ content: () => ownFile(),
102
+ project: ['.clinerules'],
103
+ home: ['Documents/Cline'],
104
+ },
105
+ {
106
+ id: 'roo',
107
+ name: 'Roo Code',
108
+ file: '.roo/rules/spectra.md',
109
+ content: () => ownFile(),
110
+ project: ['.roo', '.roorules'],
111
+ home: ['.roo'],
112
+ },
113
+ {
114
+ id: 'gemini',
115
+ name: 'Gemini CLI',
116
+ file: 'GEMINI.md',
117
+ block: () => knowledge({ nested: true }),
118
+ project: ['GEMINI.md', '.gemini'],
119
+ home: ['.gemini'],
120
+ },
121
+ ];
122
+ export const TARGET_IDS = TARGETS.map(t => t.id);
123
+
124
+ const exists = p => {
125
+ try {
126
+ fs.statSync(p);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ };
132
+
133
+ /** The agents this project or machine uses, plus AGENTS.md. */
134
+ export function detectTargets({ root, home = os.homedir() }) {
135
+ return TARGETS.filter(
136
+ t => t.always || (t.project || []).some(p => exists(path.join(root, p))) || (t.home || []).some(p => home && exists(path.join(home, p))),
137
+ ).map(t => t.id);
138
+ }
139
+
140
+ /** What `agentDocs` in spectra.config.json asks for: false (none), an array of target ids, or undefined (the default). */
141
+ export function readAgentDocsSetting(root) {
142
+ try {
143
+ const config = JSON.parse(fs.readFileSync(path.join(root, 'spectra.config.json'), 'utf8'));
144
+ return config?.agentDocs;
145
+ } catch {
146
+ return undefined;
147
+ }
148
+ }
149
+
150
+ const blockText = text => `${START}\n<!-- ${note} -->\n\n${text}\n${END}`;
151
+
152
+ // where Spectra's block is in a shared file: [start, end), or null
153
+ function findBlock(text) {
154
+ const i = text.indexOf(START);
155
+ const j = i < 0 ? -1 : text.indexOf(END, i);
156
+ return i < 0 || j < 0 ? null : [i, j + END.length];
157
+ }
158
+
159
+ // the file without Spectra's block, and the blank lines it was set apart with
160
+ function withoutBlock(text) {
161
+ const [i, j] = findBlock(text);
162
+ const head = text.slice(0, i).replace(/\s*$/, '');
163
+ const tail = text.slice(j).replace(/^\s*/, '');
164
+ const rest = head && tail ? `${head}\n\n${tail}` : head || tail;
165
+ return rest ? `${rest.replace(/\s*$/, '')}\n` : '';
166
+ }
167
+
168
+ function writeBlock(file, text, dryRun) {
169
+ const before = exists(file) ? fs.readFileSync(file, 'utf8') : null;
170
+ const block = blockText(text);
171
+ const at = before == null ? null : findBlock(before);
172
+ const after =
173
+ before == null
174
+ ? `${block}\n`
175
+ : at
176
+ ? `${before.slice(0, at[0])}${block}${before.slice(at[1])}` // in place: the rest stays exactly as it was
177
+ : `${before.replace(/\s*$/, '')}${before.trim() ? '\n\n' : ''}${block}\n`;
178
+ if (after === before) return 'unchanged';
179
+ if (!dryRun) {
180
+ fs.mkdirSync(path.dirname(file), { recursive: true });
181
+ fs.writeFileSync(file, after);
182
+ }
183
+ return before == null ? 'created' : 'updated';
184
+ }
185
+
186
+ function writeOwn(file, content, dryRun) {
187
+ const before = exists(file) ? fs.readFileSync(file, 'utf8') : null;
188
+ if (before != null && !before.includes(MANAGED)) return 'skipped (a file of yours is already there)';
189
+ if (before === content) return 'unchanged';
190
+ if (!dryRun) {
191
+ fs.mkdirSync(path.dirname(file), { recursive: true });
192
+ fs.writeFileSync(file, content);
193
+ }
194
+ return before == null ? 'created' : 'updated';
195
+ }
196
+
197
+ /**
198
+ * Writes the knowledge doc for the given targets (default: the detected ones).
199
+ * Returns [{ id, name, file, result }], result being created, updated,
200
+ * unchanged or skipped (…).
201
+ */
202
+ export function installAgentDocs({ root, targets, home, dryRun = false } = {}) {
203
+ const ids = targets || detectTargets({ root, home });
204
+ const out = [];
205
+ for (const t of TARGETS.filter(x => ids.includes(x.id))) {
206
+ const file = path.join(root, t.file);
207
+ out.push({ id: t.id, name: t.name, file: t.file, result: t.block ? writeBlock(file, t.block(), dryRun) : writeOwn(file, t.content(), dryRun) });
208
+ if (t.also) out.push({ id: t.id, name: t.name, file: t.also.file, result: writeBlock(path.join(root, t.also.file), t.also.block(), dryRun) });
209
+ }
210
+ return out;
211
+ }
212
+
213
+ // removes now-empty folders Spectra made, up to (not including) the project root
214
+ function pruneDirs(dir, root) {
215
+ while (dir.startsWith(root) && dir !== root) {
216
+ try {
217
+ if (fs.readdirSync(dir).length) return;
218
+ fs.rmdirSync(dir);
219
+ } catch {
220
+ return;
221
+ }
222
+ dir = path.dirname(dir);
223
+ }
224
+ }
225
+
226
+ /** Takes the knowledge doc out of every agent's files; the rest of each file stays. */
227
+ export function removeAgentDocs({ root, dryRun = false } = {}) {
228
+ const out = [];
229
+ const files = TARGETS.flatMap(t => [{ t, file: t.file, block: !!t.block }, ...(t.also ? [{ t, file: t.also.file, block: true }] : [])]);
230
+ for (const { t, file: rel, block } of files) {
231
+ const file = path.join(root, rel);
232
+ if (!exists(file)) continue;
233
+ const text = fs.readFileSync(file, 'utf8');
234
+ if (block) {
235
+ if (!findBlock(text)) continue;
236
+ const rest = withoutBlock(text);
237
+ if (!dryRun) {
238
+ if (rest) fs.writeFileSync(file, rest);
239
+ else fs.rmSync(file); // Spectra made it: nothing else was in it
240
+ }
241
+ out.push({ id: t.id, name: t.name, file: rel, result: rest ? 'removed' : 'deleted' });
242
+ } else if (text.includes(MANAGED)) {
243
+ if (!dryRun) {
244
+ fs.rmSync(file);
245
+ pruneDirs(path.dirname(file), root);
246
+ }
247
+ out.push({ id: t.id, name: t.name, file: rel, result: 'deleted' });
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+
253
+ /**
254
+ * Should the install step write the docs? Only for a real project install:
255
+ * not in CI, not a global install, not `npx` running Spectra from its cache,
256
+ * not Spectra's own repository, and not when the user opted out.
257
+ */
258
+ export function postinstallPlan({ env = process.env, pkgRoot = PKG_ROOT } = {}) {
259
+ const root = env.INIT_CWD && path.resolve(env.INIT_CWD);
260
+ if (env.SPECTRA_AGENT_DOCS === '0' || env.SPECTRA_AGENT_DOCS === 'false') return { skip: 'SPECTRA_AGENT_DOCS=0' };
261
+ if (env.CI && env.CI !== 'false' && env.CI !== '0') return { skip: 'CI' };
262
+ if (env.npm_config_global === 'true') return { skip: 'a global install' };
263
+ if (!root) return { skip: 'no project directory (INIT_CWD)' };
264
+ const pkg = path.resolve(pkgRoot);
265
+ if (pkg === root) return { skip: "Spectra's own repository" };
266
+ // installed into this project's node_modules (pnpm keeps it under node_modules/.pnpm), not into npx's cache
267
+ const real = (() => {
268
+ try {
269
+ return fs.realpathSync(pkg);
270
+ } catch {
271
+ return pkg;
272
+ }
273
+ })();
274
+ const modules = path.join(root, 'node_modules') + path.sep;
275
+ if (!pkg.startsWith(modules) && !real.startsWith(modules)) return { skip: 'not installed into this project (npx or a parent folder)' };
276
+ if (!exists(path.join(root, 'package.json'))) return { skip: 'no package.json' };
277
+ const setting = readAgentDocsSetting(root);
278
+ if (setting === false) return { skip: '"agentDocs": false in spectra.config.json' };
279
+ return { root, targets: Array.isArray(setting) ? setting.filter(id => TARGET_IDS.includes(id)) : undefined };
280
+ }
281
+
282
+ /** One line per file, for people. */
283
+ export const describe = results => results.map(r => ` ${r.result.padEnd(9)} ${r.file} (${r.name})`).join('\n');
@@ -66,7 +66,7 @@ export function buildFindings(report) {
66
66
  severity: 'warning',
67
67
  category: 'Setup',
68
68
  title: `Almost nothing ran during this ${(report.meta.durationMs / 1000).toFixed(0)} s recording`,
69
- detail: `Only ${fmt(totals.activeMs)} of JavaScript executed${react.detected ? ` and React committed ${commits} times` : ''}, so there is nothing slow to point at.`,
69
+ detail: `Only ${fmt(totals.activeMs)} of JavaScript executed${react.detected ? ` and ${{ vue: 'Vue', angular: 'Angular', svelte: 'Svelte' }[react.framework] || 'React'} updated components ${commits} times` : ''}, so there is nothing slow to point at.`,
70
70
  pinned: 1,
71
71
  fix: 'Press Record, then use the page (open a dashboard, scroll, type, switch tabs) before you press Stop — or use “Record page load” to profile startup.',
72
72
  });
@@ -237,7 +237,18 @@ export function buildFindings(report) {
237
237
  ? 'Svelte'
238
238
  : fw === 'angular'
239
239
  ? 'Angular'
240
- : 'React';
240
+ : fw === 'react' || fw === 'next'
241
+ ? 'React'
242
+ : 'Components';
243
+ // renders that changed nothing: the remedy in each framework's own terms
244
+ const wastedFix = unstable =>
245
+ fw === 'vue'
246
+ ? `Pass stable props${unstable ? ` (${unstable.prop} is a new ${unstable.kind} each time)` : ''}, and use v-memo for parts that rarely change.`
247
+ : fw === 'angular'
248
+ ? `Use ChangeDetectionStrategy.OnPush${unstable ? ` and keep ${unstable.prop} the same reference until its data changes` : ', and pass new references only when data changes'}.`
249
+ : unstable
250
+ ? `Wrap in React.memo, and stabilise the props that keep changing identity (${unstable.prop}: ${unstable.kind}).`
251
+ : 'Wrap in React.memo, or stop the parent re-rendering (move state down, split the component).';
241
252
  const slowFix =
242
253
  fw === 'vue'
243
254
  ? 'Move expensive work into a computed property, use v-memo/v-once for static subtrees, and virtualise long lists.'
@@ -281,9 +292,7 @@ export function buildFindings(report) {
281
292
  ),
282
293
  ],
283
294
  impactMs: c.wastedMs,
284
- fix: unstable
285
- ? `Wrap in React.memo, and stabilise the props that keep changing identity (${unstable.prop}: ${unstable.kind}).`
286
- : 'Wrap in React.memo, or stop the parent re-rendering (move state down, split the component).',
295
+ fix: wastedFix(unstable),
287
296
  });
288
297
  });
289
298
  react.components
@@ -389,11 +398,11 @@ export function buildFindings(report) {
389
398
  add({
390
399
  id: 'react-missing',
391
400
  severity: 'info',
392
- category: CAT,
393
- title: `No ${CAT === 'React' ? 'React renderer' : CAT + ' app'} was seen on this page`,
401
+ category: 'Components',
402
+ title: 'No component framework was seen on this page',
394
403
  detail:
395
- 'The probe must be installed before the framework loads (use “Reload & record”), and component-level timings need a development build.',
396
- fix: 'Click “Record page load”, or reload the tab from this tool.',
404
+ 'Files and functions are profiled for any app. For per-component data in React, Vue or Angular, the probe must be installed before the framework loads, and component timings need a development build.',
405
+ fix: 'If the app uses one of them, click “Record page load”, or reload the tab from this tool.',
397
406
  });
398
407
  }
399
408