@react-perfscope/ui 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,3025 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ App: () => App,
24
+ Panel: () => Panel,
25
+ Widget: () => Widget,
26
+ hideAllOverlays: () => hideAllOverlays,
27
+ hideOverlay: () => hideOverlay,
28
+ mount: () => mount,
29
+ mountShadow: () => mountShadow,
30
+ showOverlay: () => showOverlay
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/shadow-mount.ts
35
+ var import_preact = require("preact");
36
+ var HOST_MARKER = "data-perfscope-host";
37
+ var SCROLLBAR_CSS = `
38
+ *::-webkit-scrollbar {
39
+ width: 8px;
40
+ height: 8px;
41
+ }
42
+ *::-webkit-scrollbar-track {
43
+ background: transparent;
44
+ }
45
+ *::-webkit-scrollbar-thumb {
46
+ background: #3a3a3a;
47
+ border-radius: 4px;
48
+ border: 2px solid transparent;
49
+ background-clip: padding-box;
50
+ }
51
+ *::-webkit-scrollbar-thumb:hover {
52
+ background: #555;
53
+ background-clip: padding-box;
54
+ border: 2px solid transparent;
55
+ }
56
+ *::-webkit-scrollbar-corner {
57
+ background: transparent;
58
+ }
59
+ /* Firefox-only fallback. Chrome 121+ supports the standard scrollbar-width/
60
+ -color too, but setting them there DISABLES the ::-webkit-scrollbar styling
61
+ above (losing our 8px rounded thumb). Gate on lack of webkit support so only
62
+ non-webkit engines (Firefox) pick these up. */
63
+ @supports not selector(::-webkit-scrollbar) {
64
+ * {
65
+ scrollbar-width: thin;
66
+ scrollbar-color: #3a3a3a transparent;
67
+ }
68
+ }
69
+ `;
70
+ function mountShadow(vnode, opts = {}) {
71
+ const parent = opts.parent ?? document.body;
72
+ const host = document.createElement("div");
73
+ host.setAttribute(HOST_MARKER, "");
74
+ const root = host.attachShadow({ mode: "open" });
75
+ parent.appendChild(host);
76
+ (0, import_preact.render)(vnode, root);
77
+ const style = document.createElement("style");
78
+ style.textContent = SCROLLBAR_CSS;
79
+ root.appendChild(style);
80
+ let torn = false;
81
+ return () => {
82
+ if (torn) return;
83
+ torn = true;
84
+ try {
85
+ (0, import_preact.render)(null, root);
86
+ } catch {
87
+ }
88
+ if (host.parentNode) host.parentNode.removeChild(host);
89
+ };
90
+ }
91
+
92
+ // src/app.tsx
93
+ var import_hooks4 = require("preact/hooks");
94
+
95
+ // src/i18n.ts
96
+ var import_preact2 = require("preact");
97
+ var import_hooks = require("preact/hooks");
98
+ var STORAGE_KEY = "react-perfscope-lang";
99
+ var en = {
100
+ save: "Save",
101
+ saveAria: "Save recording",
102
+ saveTitle: "Save recording as JSON",
103
+ closeAria: "Close panel",
104
+ noSignals: "No signals recorded.",
105
+ sort: "Sort",
106
+ sortChronological: "chronological",
107
+ sortSeverity: "severity (worst first)",
108
+ groupBy: "Group by",
109
+ groupChronological: "chronological",
110
+ groupComponent: "component",
111
+ groupSource: "source",
112
+ groupCommit: "cascade (commit)",
113
+ timeline: "timeline",
114
+ value: "value",
115
+ started: "started",
116
+ duration: "duration",
117
+ size: "size",
118
+ bytes: "bytes",
119
+ renderBlocking: "render-blocking",
120
+ metric: "metric",
121
+ component: "component",
122
+ reason: "reason",
123
+ at: "at",
124
+ ended: "ended",
125
+ yes: "yes",
126
+ no: "no",
127
+ rect: "rect",
128
+ noSourceRects: "No source rects.",
129
+ noStack: "No stack captured.",
130
+ resolvingSourceMaps: "resolving source maps\u2026",
131
+ anonymous: "(anonymous)",
132
+ blocking: "blocking",
133
+ sourceCount: (n) => `${n} source(s)`,
134
+ moreItems: (n) => `+ ${n} more`,
135
+ startRecording: "Start recording",
136
+ stopRecording: "Stop recording",
137
+ rec: "rec",
138
+ signalsTitle: (n, kind) => `${n} ${kind} signals`,
139
+ noTimeBound: "No time-bound signals to plot.",
140
+ timeAxis: "time",
141
+ heapLabel: "heap",
142
+ heapUnsupported: "heap size unavailable (Chromium only)",
143
+ heapExtensionHint: "Heap size includes browser extensions injected into the page (e.g. React DevTools), so it can rise even while your app is idle. For app-only measurement, record in an incognito window or a profile with extensions disabled.",
144
+ heapTrendLabel: (cls) => cls === "leak-suspected" ? "leak suspected" : cls === "growing" ? "growing" : "stable",
145
+ fpsLabel: "fps",
146
+ fpsUnsupported: "frame timing unavailable",
147
+ fpsBadge: (minFps, dropped) => `min ${minFps}fps \xB7 ${dropped} dropped`,
148
+ fpsWorst: (ms) => `worst ${ms}ms`,
149
+ topRenderers: "Top renderers \xB7 by total time",
150
+ moreComponents: (n) => `+ ${n} more component${n === 1 ? "" : "s"}`,
151
+ rendererDetail: (c, n, total, max) => `${c} \xB7 ${n} renders \xB7 total ${total.toFixed(1)}ms \xB7 max ${max.toFixed(1)}ms`,
152
+ language: "Language",
153
+ panelRegion: "react-perfscope panel",
154
+ ratingLabel: (rating) => `rating: ${rating}`,
155
+ severityLabel: (sev) => `severity: ${sev}`,
156
+ worstLabel: (sev) => `worst: ${sev}`,
157
+ kindLabel: (kind) => kind,
158
+ reasonMounted: "mounted",
159
+ reasonState: "state changed",
160
+ reasonProps: "props changed",
161
+ reasonParent: "parent re-rendered",
162
+ cascadeRoot: "root",
163
+ changedProps: "changed",
164
+ unnecessaryRenders: (n) => `${n} unnecessary render${n === 1 ? "" : "s"} (parent-driven)`,
165
+ scripts: "scripts",
166
+ invoker: "invoker",
167
+ source: "source",
168
+ blockingTime: "blocking",
169
+ noScripts: "No script attribution (LoAF unsupported).",
170
+ hotFunctions: "your hot functions",
171
+ hotFunctionsHint: "sampled time spent in your own source",
172
+ interactionEvent: "event",
173
+ inputDelay: "input delay",
174
+ processingTime: "processing",
175
+ presentation: "presentation",
176
+ interactionThresholdHint: "Only interactions \u226540ms are shown \u2014 INP surfaces the slow ones; fast clicks are omitted."
177
+ };
178
+ var KIND_LABELS_KO = {
179
+ render: "\uB80C\uB354",
180
+ "layout-shift": "\uB808\uC774\uC544\uC6C3 \uC774\uB3D9",
181
+ "long-task": "\uAE34 \uC791\uC5C5",
182
+ "forced-reflow": "\uAC15\uC81C \uB9AC\uD50C\uB85C\uC6B0",
183
+ network: "\uB124\uD2B8\uC6CC\uD06C",
184
+ interaction: "\uC0C1\uD638\uC791\uC6A9",
185
+ "web-vital": "\uC6F9 \uBC14\uC774\uD0C8"
186
+ };
187
+ var ko = {
188
+ save: "\uC800\uC7A5",
189
+ saveAria: "\uB179\uD654 \uC800\uC7A5",
190
+ saveTitle: "\uB179\uD654\uB97C JSON\uC73C\uB85C \uC800\uC7A5",
191
+ closeAria: "\uD328\uB110 \uB2EB\uAE30",
192
+ noSignals: "\uB179\uD654\uB41C \uC2DC\uADF8\uB110\uC774 \uC5C6\uC5B4\uC694.",
193
+ sort: "\uC815\uB82C",
194
+ sortChronological: "\uC2DC\uAC04\uC21C",
195
+ sortSeverity: "\uC2EC\uAC01\uB3C4\uC21C (\uB192\uC740 \uAC83\uBD80\uD130)",
196
+ groupBy: "\uADF8\uB8F9\uD654",
197
+ groupChronological: "\uC2DC\uAC04\uC21C",
198
+ groupComponent: "\uCEF4\uD3EC\uB10C\uD2B8",
199
+ groupSource: "\uC18C\uC2A4",
200
+ groupCommit: "\uC5F0\uC1C4 (\uCEE4\uBC0B)",
201
+ timeline: "\uD0C0\uC784\uB77C\uC778",
202
+ value: "\uAC12",
203
+ started: "\uC2DC\uC791",
204
+ duration: "\uC9C0\uC18D\uC2DC\uAC04",
205
+ size: "\uD06C\uAE30",
206
+ bytes: "\uBC14\uC774\uD2B8",
207
+ renderBlocking: "\uB80C\uB354 \uCC28\uB2E8",
208
+ metric: "\uC9C0\uD45C",
209
+ component: "\uCEF4\uD3EC\uB10C\uD2B8",
210
+ reason: "\uC6D0\uC778",
211
+ at: "\uC2DC\uC810",
212
+ ended: "\uC885\uB8CC",
213
+ yes: "\uC608",
214
+ no: "\uC544\uB2C8\uC624",
215
+ rect: "\uC601\uC5ED",
216
+ noSourceRects: "\uC18C\uC2A4 \uC601\uC5ED\uC774 \uC5C6\uC5B4\uC694.",
217
+ noStack: "\uCEA1\uCC98\uB41C \uC2A4\uD0DD\uC774 \uC5C6\uC5B4\uC694.",
218
+ resolvingSourceMaps: "\uC18C\uC2A4\uB9F5 \uD574\uC11D \uC911\u2026",
219
+ anonymous: "(\uC775\uBA85)",
220
+ blocking: "\uCC28\uB2E8",
221
+ sourceCount: (n) => `\uC18C\uC2A4 ${n}\uAC1C`,
222
+ moreItems: (n) => `\uC678 ${n}\uAC1C \uB354`,
223
+ startRecording: "\uB179\uD654 \uC2DC\uC791",
224
+ stopRecording: "\uB179\uD654 \uC885\uB8CC",
225
+ rec: "\uB179\uD654",
226
+ signalsTitle: (n, kind) => `${kind} \uC2DC\uADF8\uB110 ${n}\uAC1C`,
227
+ noTimeBound: "\uD45C\uC2DC\uD560 \uC2DC\uAC04 \uAE30\uBC18 \uC2DC\uADF8\uB110\uC774 \uC5C6\uC5B4\uC694.",
228
+ timeAxis: "\uC2DC\uAC04",
229
+ heapLabel: "\uD799",
230
+ heapUnsupported: "\uD799 \uCE21\uC815 \uBBF8\uC9C0\uC6D0 (\uD06C\uB86C \uC804\uC6A9)",
231
+ heapExtensionHint: "\uD799 \uD06C\uAE30\uC5D4 \uD398\uC774\uC9C0\uC5D0 \uC8FC\uC785\uB41C \uBE0C\uB77C\uC6B0\uC800 \uD655\uC7A5(\uC608: React DevTools) \uBA54\uBAA8\uB9AC\uB3C4 \uD3EC\uD568\uB3FC\uC694. \uADF8\uB798\uC11C \uC571\uC774 idle\uC774\uC5B4\uB3C4 \uC62C\uB77C\uAC08 \uC218 \uC788\uC5B4\uC694. \uC571\uB9CC \uC815\uD655\uD788 \uC7AC\uB824\uBA74 \uC2DC\uD06C\uB9BF \uCC3D\uC774\uB098 \uD655\uC7A5\uC774 \uAEBC\uC9C4 \uD504\uB85C\uD544\uC5D0\uC11C \uB179\uD654\uD558\uC138\uC694.",
232
+ heapTrendLabel: (cls) => cls === "leak-suspected" ? "\uB204\uC218 \uC758\uC2EC" : cls === "growing" ? "\uC99D\uAC00 \uC911" : "\uC548\uC815",
233
+ fpsLabel: "fps",
234
+ fpsUnsupported: "\uD504\uB808\uC784 \uCE21\uC815 \uBBF8\uC9C0\uC6D0",
235
+ fpsBadge: (minFps, dropped) => `\uCD5C\uC800 ${minFps}fps \xB7 \uB4DC\uB78D ${dropped}`,
236
+ fpsWorst: (ms) => `\uCD5C\uC545 ${ms}ms`,
237
+ topRenderers: "\uC0C1\uC704 \uB80C\uB354\uB7EC \xB7 \uCD1D \uC2DC\uAC04\uC21C",
238
+ moreComponents: (n) => `\uC678 \uCEF4\uD3EC\uB10C\uD2B8 ${n}\uAC1C \uB354`,
239
+ rendererDetail: (c, n, total, max) => `${c} \xB7 \uB80C\uB354 ${n}\uD68C \xB7 \uCD1D ${total.toFixed(1)}ms \xB7 \uCD5C\uB300 ${max.toFixed(1)}ms`,
240
+ language: "\uC5B8\uC5B4",
241
+ panelRegion: "react-perfscope \uD328\uB110",
242
+ ratingLabel: (rating) => `\uD3C9\uAC00: ${rating}`,
243
+ severityLabel: (sev) => `\uC2EC\uAC01\uB3C4: ${sev}`,
244
+ worstLabel: (sev) => `\uAC00\uC7A5 \uC2EC\uAC01: ${sev}`,
245
+ kindLabel: (kind) => KIND_LABELS_KO[kind],
246
+ reasonMounted: "\uB9C8\uC6B4\uD2B8\uB428",
247
+ reasonState: "state \uBCC0\uACBD",
248
+ reasonProps: "props \uBCC0\uACBD",
249
+ reasonParent: "\uBD80\uBAA8 \uB530\uB77C \uB9AC\uB80C\uB354",
250
+ cascadeRoot: "\uC2DC\uC791\uC810",
251
+ changedProps: "\uBCC0\uACBD\uB41C props",
252
+ unnecessaryRenders: (n) => `\uBD88\uD544\uC694\uD55C \uB9AC\uB80C\uB354 ${n}\uAC1C (\uBD80\uBAA8 \uB54C\uBB38\uC5D0)`,
253
+ scripts: "\uC2A4\uD06C\uB9BD\uD2B8",
254
+ invoker: "\uD638\uCD9C\uC790",
255
+ source: "\uC18C\uC2A4",
256
+ blockingTime: "\uCC28\uB2E8 \uC2DC\uAC04",
257
+ noScripts: "\uC2A4\uD06C\uB9BD\uD2B8 \uCD9C\uCC98 \uC5C6\uC74C (LoAF \uBBF8\uC9C0\uC6D0).",
258
+ hotFunctions: "\uB0B4 \uCF54\uB4DC \uD56B\uC2A4\uD31F",
259
+ hotFunctionsHint: "\uB0B4 \uC18C\uC2A4\uC5D0\uC11C \uC0D8\uD50C\uB9C1\uB41C \uC810\uC720 \uC2DC\uAC04",
260
+ interactionEvent: "\uC774\uBCA4\uD2B8",
261
+ inputDelay: "\uC785\uB825 \uC9C0\uC5F0",
262
+ processingTime: "\uCC98\uB9AC",
263
+ presentation: "\uD654\uBA74 \uBC18\uC601",
264
+ interactionThresholdHint: "40ms \uC774\uC0C1 \uAC78\uB9B0 \uC0C1\uD638\uC791\uC6A9\uB9CC \uD45C\uC2DC\uB3FC\uC694 \u2014 \uB290\uB9B0 \uAC83\uB9CC INP\uB85C \uC7A1\uACE0, \uBE60\uB978 \uD074\uB9AD\uC740 \uC0DD\uB7B5\uD574\uC694."
265
+ };
266
+ var STRINGS = { en, ko };
267
+ function isLang(v) {
268
+ return v === "en" || v === "ko";
269
+ }
270
+ function readStoredLang() {
271
+ if (typeof localStorage === "undefined") return "en";
272
+ try {
273
+ const v = localStorage.getItem(STORAGE_KEY);
274
+ return isLang(v) ? v : "en";
275
+ } catch {
276
+ return "en";
277
+ }
278
+ }
279
+ var I18nContext = (0, import_preact2.createContext)({
280
+ lang: "en",
281
+ setLang: () => {
282
+ },
283
+ t: en
284
+ });
285
+ function I18nProvider({ children }) {
286
+ const [lang, setLangState] = (0, import_hooks.useState)(() => readStoredLang());
287
+ const setLang = (0, import_hooks.useCallback)((l) => {
288
+ setLangState(l);
289
+ if (typeof localStorage !== "undefined") {
290
+ try {
291
+ localStorage.setItem(STORAGE_KEY, l);
292
+ } catch {
293
+ }
294
+ }
295
+ }, []);
296
+ const value = (0, import_hooks.useMemo)(() => ({ lang, setLang, t: STRINGS[lang] }), [lang, setLang]);
297
+ return (0, import_preact2.h)(I18nContext.Provider, { value }, children);
298
+ }
299
+ function useI18n() {
300
+ return (0, import_hooks.useContext)(I18nContext);
301
+ }
302
+
303
+ // src/widget.tsx
304
+ var import_jsx_runtime = require("preact/jsx-runtime");
305
+ var POSITION_STYLES = {
306
+ "bottom-right": { bottom: "16px", right: "16px" },
307
+ "bottom-left": { bottom: "16px", left: "16px" },
308
+ "top-right": { top: "16px", right: "16px" },
309
+ "top-left": { top: "16px", left: "16px" }
310
+ };
311
+ function formatElapsed(ms) {
312
+ const totalSeconds = Math.floor(ms / 1e3);
313
+ const minutes = Math.floor(totalSeconds / 60);
314
+ const seconds = totalSeconds - minutes * 60;
315
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
316
+ }
317
+ function Widget(props) {
318
+ const { recording, elapsedMs = 0, onToggle, position = "bottom-right" } = props;
319
+ const positionStyle = POSITION_STYLES[position];
320
+ const { t } = useI18n();
321
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
322
+ "div",
323
+ {
324
+ "data-position": position,
325
+ style: {
326
+ position: "fixed",
327
+ ...positionStyle,
328
+ zIndex: "2147483647",
329
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
330
+ },
331
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
332
+ "button",
333
+ {
334
+ type: "button",
335
+ "aria-pressed": recording,
336
+ "aria-label": recording ? t.stopRecording : t.startRecording,
337
+ onClick: onToggle,
338
+ style: {
339
+ background: "#1a1a1a",
340
+ color: "#e6e6e6",
341
+ border: "1px solid #2a2a2a",
342
+ borderRadius: "20px",
343
+ padding: "8px 14px",
344
+ fontSize: "12px",
345
+ cursor: "pointer",
346
+ display: "flex",
347
+ alignItems: "center",
348
+ gap: "8px",
349
+ boxShadow: "0 4px 12px rgba(0,0,0,0.2)"
350
+ },
351
+ children: [
352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
353
+ "span",
354
+ {
355
+ style: {
356
+ width: "8px",
357
+ height: "8px",
358
+ borderRadius: "50%",
359
+ background: recording ? "#ff3b30" : "#666",
360
+ display: "inline-block"
361
+ }
362
+ }
363
+ ),
364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-live": "polite", children: recording ? formatElapsed(elapsedMs) : t.rec })
365
+ ]
366
+ }
367
+ )
368
+ }
369
+ );
370
+ }
371
+
372
+ // src/panel.tsx
373
+ var import_hooks3 = require("preact/hooks");
374
+
375
+ // src/overlay.ts
376
+ var OVERLAY_MARKER = "data-perfscope-overlay";
377
+ var ARROW_MARKER = "data-perfscope-overlay-arrow";
378
+ var FADE_MS = 80;
379
+ var DEFAULT_BORDER = "#ff3b30";
380
+ var DEFAULT_FILL = "rgba(255, 59, 48, 0.12)";
381
+ function getOrCreate(id) {
382
+ const existing = document.querySelector(`[${OVERLAY_MARKER}="${id}"]`);
383
+ if (existing) return existing;
384
+ const el = document.createElement("div");
385
+ el.setAttribute(OVERLAY_MARKER, id);
386
+ el.style.position = "absolute";
387
+ el.style.pointerEvents = "none";
388
+ el.style.boxSizing = "border-box";
389
+ el.style.border = `2px solid ${DEFAULT_BORDER}`;
390
+ el.style.background = DEFAULT_FILL;
391
+ el.style.zIndex = "2147483646";
392
+ el.style.borderRadius = "2px";
393
+ el.style.transition = `opacity ${FADE_MS}ms ease-out`;
394
+ document.body.appendChild(el);
395
+ return el;
396
+ }
397
+ function showOverlay(id, rect, style) {
398
+ const el = getOrCreate(id);
399
+ el.style.left = `${rect.left}px`;
400
+ el.style.top = `${rect.top}px`;
401
+ el.style.width = `${rect.width}px`;
402
+ el.style.height = `${rect.height}px`;
403
+ el.style.opacity = "1";
404
+ const border = style?.border ?? DEFAULT_BORDER;
405
+ const fill = style?.fill ?? DEFAULT_FILL;
406
+ el.style.borderStyle = style?.dashed ? "dashed" : "solid";
407
+ el.style.borderColor = border;
408
+ el.style.background = fill;
409
+ }
410
+ function showArrow(id, from, to, color = DEFAULT_BORDER) {
411
+ let svg = document.querySelector(`[${ARROW_MARKER}="${id}"]`);
412
+ if (!svg) {
413
+ svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
414
+ svg.setAttribute(ARROW_MARKER, id);
415
+ svg.style.position = "absolute";
416
+ svg.style.pointerEvents = "none";
417
+ svg.style.overflow = "visible";
418
+ svg.style.zIndex = "2147483646";
419
+ svg.style.transition = `opacity ${FADE_MS}ms ease-out`;
420
+ document.body.appendChild(svg);
421
+ }
422
+ const minX = Math.min(from.x, to.x);
423
+ const minY = Math.min(from.y, to.y);
424
+ const w = Math.max(2, Math.abs(to.x - from.x));
425
+ const h3 = Math.max(2, Math.abs(to.y - from.y));
426
+ svg.style.left = `${minX}px`;
427
+ svg.style.top = `${minY}px`;
428
+ svg.setAttribute("width", `${w}`);
429
+ svg.setAttribute("height", `${h3}`);
430
+ svg.setAttribute("viewBox", `0 0 ${w} ${h3}`);
431
+ svg.style.opacity = "1";
432
+ while (svg.firstChild) svg.removeChild(svg.firstChild);
433
+ const fx = from.x - minX;
434
+ const fy = from.y - minY;
435
+ const tx = to.x - minX;
436
+ const ty = to.y - minY;
437
+ const markerId = `perfscope-arrowhead-${id}`;
438
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
439
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
440
+ marker.setAttribute("id", markerId);
441
+ marker.setAttribute("viewBox", "0 0 10 10");
442
+ marker.setAttribute("refX", "8");
443
+ marker.setAttribute("refY", "5");
444
+ marker.setAttribute("markerWidth", "6");
445
+ marker.setAttribute("markerHeight", "6");
446
+ marker.setAttribute("orient", "auto-start-reverse");
447
+ const arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
448
+ arrowPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
449
+ arrowPath.setAttribute("fill", color);
450
+ marker.appendChild(arrowPath);
451
+ defs.appendChild(marker);
452
+ svg.appendChild(defs);
453
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
454
+ line.setAttribute("x1", `${fx}`);
455
+ line.setAttribute("y1", `${fy}`);
456
+ line.setAttribute("x2", `${tx}`);
457
+ line.setAttribute("y2", `${ty}`);
458
+ line.setAttribute("stroke", color);
459
+ line.setAttribute("stroke-width", "3");
460
+ line.setAttribute("stroke-linecap", "round");
461
+ line.setAttribute("marker-end", `url(#${markerId})`);
462
+ svg.appendChild(line);
463
+ }
464
+ function hideArrow(id) {
465
+ const svg = document.querySelector(`[${ARROW_MARKER}="${id}"]`);
466
+ if (svg) svg.remove();
467
+ }
468
+ function hideOverlay(id) {
469
+ const el = document.querySelector(`[${OVERLAY_MARKER}="${id}"]`);
470
+ if (!el) return;
471
+ el.style.opacity = "0";
472
+ setTimeout(() => {
473
+ const stillThere = document.querySelector(`[${OVERLAY_MARKER}="${id}"]`);
474
+ if (stillThere && stillThere.style.opacity === "0") {
475
+ stillThere.remove();
476
+ }
477
+ }, FADE_MS + 20);
478
+ }
479
+ function hideAllOverlays() {
480
+ for (const el of Array.from(document.querySelectorAll(`[${OVERLAY_MARKER}]`))) {
481
+ el.remove();
482
+ }
483
+ for (const el of Array.from(document.querySelectorAll(`[${ARROW_MARKER}]`))) {
484
+ el.remove();
485
+ }
486
+ }
487
+
488
+ // src/severity.ts
489
+ var SEVERITY_COLOR = {
490
+ low: "#666",
491
+ medium: "#ff9500",
492
+ high: "#ff3b30"
493
+ };
494
+ var SEVERITY_OVERLAY_COLOR = {
495
+ low: "#3b82f6",
496
+ medium: "#ff9500",
497
+ high: "#ff3b30"
498
+ };
499
+ var RATING_COLOR = {
500
+ good: "#34c759",
501
+ needs: "#ff9500",
502
+ poor: "#ff3b30"
503
+ };
504
+ var WEB_VITAL_THRESHOLDS = {
505
+ LCP: [2500, 4e3],
506
+ INP: [200, 500],
507
+ CLS: [0.1, 0.25],
508
+ FCP: [1800, 3e3],
509
+ TTFB: [800, 1800]
510
+ };
511
+ function webVitalRating(name, value) {
512
+ const [good, needs] = WEB_VITAL_THRESHOLDS[name];
513
+ if (value <= good) return "good";
514
+ if (value <= needs) return "needs";
515
+ return "poor";
516
+ }
517
+ var SEVERITY_RANK = { low: 0, medium: 1, high: 2 };
518
+ function severityForSignal(s) {
519
+ switch (s.kind) {
520
+ case "long-task":
521
+ if (s.duration >= 100) return "high";
522
+ if (s.duration >= 50) return "medium";
523
+ return "low";
524
+ case "forced-reflow":
525
+ if (s.duration >= 20) return "high";
526
+ if (s.duration >= 5) return "medium";
527
+ return "low";
528
+ case "layout-shift":
529
+ if (s.value >= 0.1) return "high";
530
+ if (s.value >= 0.05) return "medium";
531
+ return "low";
532
+ case "render":
533
+ if (s.duration >= 33) return "high";
534
+ if (s.duration >= 16) return "medium";
535
+ return "low";
536
+ case "network":
537
+ if (s.blocking && s.duration >= 500) return "high";
538
+ if (s.duration >= 1e3) return "high";
539
+ if (s.duration >= 500) return "medium";
540
+ return "low";
541
+ case "web-vital": {
542
+ const rating = webVitalRating(s.name, s.value);
543
+ return rating === "poor" ? "high" : rating === "needs" ? "medium" : "low";
544
+ }
545
+ case "interaction":
546
+ if (s.duration >= 500) return "high";
547
+ if (s.duration >= 200) return "medium";
548
+ return "low";
549
+ }
550
+ }
551
+ function worstSeverity(signals) {
552
+ let worst = "low";
553
+ for (const s of signals) {
554
+ const sev = severityForSignal(s);
555
+ if (SEVERITY_RANK[sev] > SEVERITY_RANK[worst]) worst = sev;
556
+ }
557
+ return worst;
558
+ }
559
+ function severityRank(sev) {
560
+ return SEVERITY_RANK[sev];
561
+ }
562
+
563
+ // src/summary.tsx
564
+ var import_jsx_runtime2 = require("preact/jsx-runtime");
565
+ var WEB_VITAL_UNIT = {
566
+ LCP: "ms",
567
+ INP: "ms",
568
+ CLS: "",
569
+ FCP: "ms",
570
+ TTFB: "ms"
571
+ };
572
+ function formatVitalValue(name, value) {
573
+ if (name === "CLS") return value.toFixed(3);
574
+ if (value >= 1e3) return (value / 1e3).toFixed(2) + "s";
575
+ return value.toFixed(0);
576
+ }
577
+ function VitalChip({ s }) {
578
+ const { t } = useI18n();
579
+ const rating = webVitalRating(s.name, s.value);
580
+ const color = RATING_COLOR[rating];
581
+ const unit = WEB_VITAL_UNIT[s.name];
582
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
583
+ "span",
584
+ {
585
+ "data-vital": s.name,
586
+ "data-rating": rating,
587
+ style: {
588
+ display: "inline-flex",
589
+ alignItems: "center",
590
+ gap: "5px",
591
+ padding: "3px 8px",
592
+ borderRadius: "999px",
593
+ background: "#1a1a1a",
594
+ border: `1px solid ${color}33`,
595
+ fontSize: "11px",
596
+ fontFamily: "SF Mono, Menlo, Consolas, monospace"
597
+ },
598
+ children: [
599
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
600
+ "span",
601
+ {
602
+ "aria-label": t.ratingLabel(rating),
603
+ style: {
604
+ width: "6px",
605
+ height: "6px",
606
+ borderRadius: "50%",
607
+ background: color,
608
+ flex: "0 0 6px"
609
+ }
610
+ }
611
+ ),
612
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { style: { color: "#e6e6e6" }, children: s.name }),
613
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color }, children: [
614
+ formatVitalValue(s.name, s.value),
615
+ unit !== "s" ? unit : ""
616
+ ] })
617
+ ]
618
+ }
619
+ );
620
+ }
621
+ function SummaryHeader({ signals, grouped, kindsPresent, onKindClick }) {
622
+ const { t } = useI18n();
623
+ const latestVitals = /* @__PURE__ */ new Map();
624
+ for (const s of signals) {
625
+ if (s.kind === "web-vital") latestVitals.set(s.name, s);
626
+ }
627
+ const vitals = Array.from(latestVitals.values());
628
+ const nonVitalKinds = kindsPresent.filter((k) => k !== "web-vital");
629
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
630
+ "div",
631
+ {
632
+ style: {
633
+ marginBottom: "8px",
634
+ padding: "8px 10px",
635
+ background: "#141414",
636
+ border: "1px solid #1f1f1f",
637
+ borderRadius: "8px",
638
+ display: "flex",
639
+ flexDirection: "column",
640
+ gap: "6px"
641
+ },
642
+ children: [
643
+ vitals.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px" }, children: vitals.map((v) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VitalChip, { s: v }, v.name)) }),
644
+ nonVitalKinds.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
645
+ "div",
646
+ {
647
+ style: {
648
+ display: "flex",
649
+ flexWrap: "wrap",
650
+ gap: "10px",
651
+ fontSize: "11px",
652
+ color: "#aaa",
653
+ fontFamily: "SF Mono, Menlo, Consolas, monospace"
654
+ },
655
+ children: nonVitalKinds.map((k) => {
656
+ const list = grouped[k];
657
+ const sev = worstSeverity(list);
658
+ const color = sev === "low" ? "#aaa" : SEVERITY_COLOR[sev];
659
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
660
+ "span",
661
+ {
662
+ "data-summary-kind": k,
663
+ onClick: () => onKindClick?.(k),
664
+ style: {
665
+ cursor: onKindClick ? "pointer" : "default",
666
+ display: "inline-flex",
667
+ alignItems: "center",
668
+ gap: "4px"
669
+ },
670
+ title: t.signalsTitle(list.length, t.kindLabel(k)),
671
+ children: [
672
+ sev !== "low" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
673
+ "span",
674
+ {
675
+ style: {
676
+ width: "6px",
677
+ height: "6px",
678
+ borderRadius: "50%",
679
+ background: color,
680
+ flex: "0 0 6px"
681
+ }
682
+ }
683
+ ),
684
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t.kindLabel(k) }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { style: { color }, children: list.length })
686
+ ]
687
+ },
688
+ k
689
+ );
690
+ })
691
+ }
692
+ )
693
+ ]
694
+ }
695
+ );
696
+ }
697
+
698
+ // src/timeline.tsx
699
+ var import_preact3 = require("preact");
700
+ var import_hooks2 = require("preact/hooks");
701
+ var import_core = require("@react-perfscope/core");
702
+ var import_jsx_runtime3 = require("preact/jsx-runtime");
703
+ var LANE_ORDER = [
704
+ "long-task",
705
+ "interaction",
706
+ "forced-reflow",
707
+ "layout-shift",
708
+ "render",
709
+ "network"
710
+ ];
711
+ function signalAbsoluteTime(s) {
712
+ if (s.kind === "web-vital") return null;
713
+ if (s.kind === "network") return s.startedAt;
714
+ return s.at;
715
+ }
716
+ function signalDuration(s) {
717
+ if (s.kind === "long-task" || s.kind === "forced-reflow" || s.kind === "render" || s.kind === "network" || s.kind === "interaction") {
718
+ return s.duration;
719
+ }
720
+ return 0;
721
+ }
722
+ function formatTime(ms) {
723
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
724
+ if (ms >= 1) return `${ms.toFixed(0)}ms`;
725
+ return `${ms.toFixed(1)}ms`;
726
+ }
727
+ function niceStep(span, target) {
728
+ const raw = Math.max(span, 1) / target;
729
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
730
+ const norm = raw / mag;
731
+ if (norm < 1.5) return 1 * mag;
732
+ if (norm < 3) return 2 * mag;
733
+ if (norm < 7) return 5 * mag;
734
+ return 10 * mag;
735
+ }
736
+ function niceTicks(start, end, target = 5) {
737
+ const span = Math.max(end - start, 1);
738
+ const step = niceStep(span, target);
739
+ const first = Math.ceil(start / step) * step;
740
+ const ticks = [];
741
+ for (let t = first; t <= end + 1e-6; t += step) ticks.push(t);
742
+ return ticks;
743
+ }
744
+ var LANE_HEIGHT = 28;
745
+ var NET_BAR_H = 7;
746
+ var NET_ROW_GAP = 3;
747
+ var NET_ROW_BRIGHTNESS = [1, 0.78, 1.22, 0.9, 1.12];
748
+ var LABEL_COL_WIDTH = 124;
749
+ var AXIS_HEIGHT = 22;
750
+ var TRACK_PAD_X = 4;
751
+ var MIN_BAR_W = 6;
752
+ var MARKER_D = 9;
753
+ var CLUSTER_BIN_MS = 16;
754
+ var MIN_PX_PER_SEC = 80;
755
+ function plotLane(signals, kind, winStart, winEnd, startedAt) {
756
+ const span = Math.max(winEnd - winStart, 1);
757
+ const out = [];
758
+ for (const s of signals) {
759
+ if (s.kind !== kind) continue;
760
+ const tAbs = signalAbsoluteTime(s);
761
+ if (tAbs == null) continue;
762
+ const t = tAbs - startedAt - winStart;
763
+ const d = signalDuration(s);
764
+ const startFrac = Math.max(0, Math.min(1, t / span));
765
+ const widthFrac = Math.max(0, Math.min(1 - startFrac, d / span));
766
+ out.push({ s, startFrac, widthFrac });
767
+ }
768
+ return out;
769
+ }
770
+ function packRows(bars, trackInnerPx) {
771
+ const GAP_PX = 2;
772
+ const rowEnds = [];
773
+ const rows = [];
774
+ for (const b of bars) {
775
+ const startPx = b.startFrac * trackInnerPx;
776
+ const endPx = startPx + Math.max(MIN_BAR_W, b.widthFrac * trackInnerPx);
777
+ let placed = -1;
778
+ for (let r = 0; r < rowEnds.length; r++) {
779
+ if (startPx >= rowEnds[r] + GAP_PX) {
780
+ placed = r;
781
+ break;
782
+ }
783
+ }
784
+ if (placed === -1) {
785
+ placed = rowEnds.length;
786
+ rowEnds.push(0);
787
+ }
788
+ rowEnds[placed] = endPx;
789
+ rows.push(placed);
790
+ }
791
+ return { rows, rowCount: Math.max(1, rowEnds.length) };
792
+ }
793
+ var SEV_RANK = { low: 0, medium: 1, high: 2 };
794
+ function clusterLane(items, binFrac) {
795
+ const bars = [];
796
+ const pts = [];
797
+ for (const it of items) {
798
+ if (it.widthFrac >= binFrac) bars.push(it);
799
+ else pts.push(it);
800
+ }
801
+ pts.sort((a, b) => a.startFrac - b.startFrac);
802
+ const clusters = [];
803
+ let i = 0;
804
+ while (i < pts.length) {
805
+ const start = pts[i].startFrac;
806
+ let rep = pts[i].s;
807
+ let worst = severityForSignal(pts[i].s);
808
+ let sum = 0;
809
+ let j = i;
810
+ const members = [];
811
+ while (j < pts.length && pts[j].startFrac - start <= binFrac) {
812
+ const sev = severityForSignal(pts[j].s);
813
+ if (SEV_RANK[sev] > SEV_RANK[worst]) {
814
+ worst = sev;
815
+ rep = pts[j].s;
816
+ }
817
+ members.push(pts[j].s);
818
+ sum += pts[j].startFrac;
819
+ j++;
820
+ }
821
+ clusters.push({ startFrac: sum / members.length, count: members.length, rep, worst, members });
822
+ i = j;
823
+ }
824
+ return { bars, clusters };
825
+ }
826
+ function renderBreakdown(members) {
827
+ const order = [];
828
+ const counts = /* @__PURE__ */ new Map();
829
+ for (const s of members) {
830
+ if (s.kind !== "render") continue;
831
+ if (!counts.has(s.component)) order.push(s.component);
832
+ counts.set(s.component, (counts.get(s.component) ?? 0) + 1);
833
+ }
834
+ return order.map((c) => ({ component: c, count: counts.get(c) }));
835
+ }
836
+ var HEAP_STRIP_H = 46;
837
+ var HEAP_VB_W = 100;
838
+ var HEAP_VB_H = 100;
839
+ var HEAP_TREND_COLOR = {
840
+ stable: "#3b82f6",
841
+ growing: "#f59e0b",
842
+ "leak-suspected": "#ef4444"
843
+ };
844
+ function formatBytes(b) {
845
+ if (b >= 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(1)}MB`;
846
+ if (b >= 1024) return `${(b / 1024).toFixed(0)}KB`;
847
+ return `${b}B`;
848
+ }
849
+ function buildHeapPaths(samples, startedAt, safeDur) {
850
+ if (samples.length === 0) return null;
851
+ let lo = Infinity;
852
+ let hi = -Infinity;
853
+ for (const s of samples) {
854
+ if (s.used < lo) lo = s.used;
855
+ if (s.used > hi) hi = s.used;
856
+ }
857
+ const range = Math.max(hi - lo, 1);
858
+ const pts = samples.map((s) => {
859
+ const x = Math.max(0, Math.min(HEAP_VB_W, (s.at - startedAt) / safeDur * HEAP_VB_W));
860
+ const y = HEAP_VB_H - (s.used - lo) / range * HEAP_VB_H;
861
+ return { x, y };
862
+ });
863
+ const line = pts.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
864
+ const last = pts[pts.length - 1];
865
+ const first = pts[0];
866
+ const area = `${line} L${last.x.toFixed(2)} ${HEAP_VB_H} L${first.x.toFixed(2)} ${HEAP_VB_H} Z`;
867
+ return { area, line, min: lo, max: hi };
868
+ }
869
+ function HeapStrip({
870
+ samples,
871
+ startedAt,
872
+ safeDur,
873
+ trackWidth,
874
+ scrolls
875
+ }) {
876
+ const { t } = useI18n();
877
+ const has = !!samples && samples.length > 0;
878
+ const trend = (0, import_hooks2.useMemo)(() => has ? (0, import_core.analyzeHeapTrend)(samples) : null, [samples, has]);
879
+ const paths = (0, import_hooks2.useMemo)(
880
+ () => has ? buildHeapPaths(samples, startedAt, safeDur) : null,
881
+ [samples, has, startedAt, safeDur]
882
+ );
883
+ const color = trend ? HEAP_TREND_COLOR[trend.classification] : "#3b82f6";
884
+ const [hintAnchor, setHintAnchor] = (0, import_hooks2.useState)(
885
+ null
886
+ );
887
+ const hintRef = (0, import_hooks2.useRef)(null);
888
+ const [hintPos, setHintPos] = (0, import_hooks2.useState)(null);
889
+ (0, import_hooks2.useLayoutEffect)(() => {
890
+ if (!hintAnchor) {
891
+ setHintPos(null);
892
+ return;
893
+ }
894
+ const el = hintRef.current;
895
+ if (!el) return;
896
+ const r = el.getBoundingClientRect();
897
+ const vw = window.innerWidth;
898
+ const vh = window.innerHeight;
899
+ const gap = 6;
900
+ let left = hintAnchor.left;
901
+ if (left + r.width + 8 > vw) left = vw - r.width - 8;
902
+ if (left < 8) left = 8;
903
+ let top = hintAnchor.bottom + gap;
904
+ if (top + r.height + 8 > vh) top = hintAnchor.top - r.height - gap;
905
+ if (top < 8) top = 8;
906
+ setHintPos({ left, top });
907
+ }, [hintAnchor]);
908
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
909
+ "div",
910
+ {
911
+ "data-heap-strip": "",
912
+ style: {
913
+ display: "flex",
914
+ alignItems: "stretch",
915
+ height: `${HEAP_STRIP_H}px`,
916
+ background: "#111",
917
+ borderBottom: "1px solid #1c1c1c"
918
+ },
919
+ children: [
920
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
921
+ "div",
922
+ {
923
+ style: {
924
+ flex: `0 0 ${LABEL_COL_WIDTH}px`,
925
+ minWidth: 0,
926
+ boxSizing: "border-box",
927
+ position: "sticky",
928
+ left: 0,
929
+ zIndex: 5,
930
+ display: "flex",
931
+ flexDirection: "column",
932
+ justifyContent: "center",
933
+ gap: "2px",
934
+ padding: "0 8px",
935
+ borderRight: "1px solid #1c1c1c",
936
+ background: "#0f0f0f"
937
+ },
938
+ children: [
939
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: "4px", color: "#9a9a9a" }, children: [
940
+ t.heapLabel,
941
+ has && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
942
+ "span",
943
+ {
944
+ "data-heap-hint": "",
945
+ "aria-label": t.heapExtensionHint,
946
+ onMouseEnter: (e) => {
947
+ const r = e.currentTarget.getBoundingClientRect();
948
+ setHintAnchor({ left: r.left, top: r.top, bottom: r.bottom });
949
+ },
950
+ onMouseLeave: () => setHintAnchor(null),
951
+ style: {
952
+ display: "inline-flex",
953
+ alignItems: "center",
954
+ justifyContent: "center",
955
+ width: "12px",
956
+ height: "12px",
957
+ borderRadius: "50%",
958
+ border: "1px solid #555",
959
+ color: "#888",
960
+ fontSize: "9px",
961
+ lineHeight: 1,
962
+ cursor: "help"
963
+ },
964
+ children: "i"
965
+ }
966
+ )
967
+ ] }),
968
+ has && trend && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { "data-heap-trend": trend.classification, style: { color, fontSize: "10px" }, children: [
969
+ t.heapTrendLabel(trend.classification),
970
+ trend.classification !== "stable" && ` \xB7 +${(trend.slopeBytesPerMin / (1024 * 1024)).toFixed(1)}MB/min`
971
+ ] })
972
+ ]
973
+ }
974
+ ),
975
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
976
+ "div",
977
+ {
978
+ style: {
979
+ flex: scrolls ? "0 0 auto" : 1,
980
+ width: scrolls ? `${trackWidth}px` : void 0,
981
+ position: "relative",
982
+ padding: `0 ${TRACK_PAD_X}px`
983
+ },
984
+ children: has && paths ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_preact3.Fragment, { children: [
985
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
986
+ "svg",
987
+ {
988
+ width: "100%",
989
+ height: HEAP_STRIP_H,
990
+ viewBox: `0 0 ${HEAP_VB_W} ${HEAP_VB_H}`,
991
+ preserveAspectRatio: "none",
992
+ style: { display: "block" },
993
+ "aria-hidden": "true",
994
+ children: [
995
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: paths.area, fill: color, fillOpacity: 0.16 }),
996
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: paths.line, fill: "none", stroke: color, strokeWidth: 1.2, vectorEffect: "non-scaling-stroke" })
997
+ ]
998
+ }
999
+ ),
1000
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1001
+ "span",
1002
+ {
1003
+ style: {
1004
+ position: "absolute",
1005
+ top: "2px",
1006
+ right: "6px",
1007
+ color: "#666",
1008
+ fontSize: "10px",
1009
+ pointerEvents: "none"
1010
+ },
1011
+ children: [
1012
+ formatBytes(paths.min),
1013
+ "\u2013",
1014
+ formatBytes(paths.max)
1015
+ ]
1016
+ }
1017
+ )
1018
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1019
+ "span",
1020
+ {
1021
+ style: {
1022
+ position: "absolute",
1023
+ top: "50%",
1024
+ left: "8px",
1025
+ transform: "translateY(-50%)",
1026
+ color: "#666",
1027
+ fontSize: "10px"
1028
+ },
1029
+ children: t.heapUnsupported
1030
+ }
1031
+ )
1032
+ }
1033
+ ),
1034
+ hintAnchor && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1035
+ "div",
1036
+ {
1037
+ ref: hintRef,
1038
+ role: "tooltip",
1039
+ style: {
1040
+ position: "fixed",
1041
+ left: `${hintPos ? hintPos.left : 0}px`,
1042
+ top: `${hintPos ? hintPos.top : 0}px`,
1043
+ visibility: hintPos ? "visible" : "hidden",
1044
+ width: "220px",
1045
+ padding: "7px 9px",
1046
+ background: "#0d0d0d",
1047
+ border: "1px solid #2a2a2a",
1048
+ borderRadius: "6px",
1049
+ color: "#cfcfcf",
1050
+ fontSize: "10px",
1051
+ fontWeight: 400,
1052
+ lineHeight: 1.5,
1053
+ whiteSpace: "normal",
1054
+ boxShadow: "0 4px 14px rgba(0,0,0,0.5)",
1055
+ zIndex: 2147483647,
1056
+ pointerEvents: "none"
1057
+ },
1058
+ children: t.heapExtensionHint
1059
+ }
1060
+ )
1061
+ ]
1062
+ }
1063
+ );
1064
+ }
1065
+ var FPS_GOOD = "#34c759";
1066
+ var FPS_OK = "#f59e0b";
1067
+ var FPS_BAD = "#ef4444";
1068
+ function fpsColor(minFps) {
1069
+ if (minFps >= 50) return FPS_GOOD;
1070
+ if (minFps >= 30) return FPS_OK;
1071
+ return FPS_BAD;
1072
+ }
1073
+ function buildFpsPaths(series, startedAt, safeDur) {
1074
+ if (series.length === 0) return null;
1075
+ let max = 60;
1076
+ for (const s of series) if (s.fps > max) max = s.fps;
1077
+ const pts = series.map((s) => {
1078
+ const x = Math.max(0, Math.min(HEAP_VB_W, (s.at - startedAt) / safeDur * HEAP_VB_W));
1079
+ const y = HEAP_VB_H - Math.min(s.fps, max) / max * HEAP_VB_H;
1080
+ return { x, y };
1081
+ });
1082
+ if (pts.length === 1) pts.push({ x: HEAP_VB_W, y: pts[0].y });
1083
+ pts[0].x = 0;
1084
+ pts[pts.length - 1].x = HEAP_VB_W;
1085
+ const line = pts.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
1086
+ const last = pts[pts.length - 1];
1087
+ const first = pts[0];
1088
+ const area = `${line} L${last.x.toFixed(2)} ${HEAP_VB_H} L${first.x.toFixed(2)} ${HEAP_VB_H} Z`;
1089
+ return { area, line, max };
1090
+ }
1091
+ function FpsStrip({
1092
+ frames,
1093
+ startedAt,
1094
+ safeDur,
1095
+ trackWidth,
1096
+ scrolls
1097
+ }) {
1098
+ const { t } = useI18n();
1099
+ const has = !!frames && frames.length > 0;
1100
+ const stats = (0, import_hooks2.useMemo)(() => has ? (0, import_core.analyzeFrames)(frames) : null, [frames, has]);
1101
+ const paths = (0, import_hooks2.useMemo)(
1102
+ () => stats ? buildFpsPaths(stats.series, startedAt, safeDur) : null,
1103
+ [stats, startedAt, safeDur]
1104
+ );
1105
+ const color = stats ? fpsColor(stats.minFps) : FPS_GOOD;
1106
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1107
+ "div",
1108
+ {
1109
+ "data-fps-strip": "",
1110
+ style: {
1111
+ display: "flex",
1112
+ alignItems: "stretch",
1113
+ height: `${HEAP_STRIP_H}px`,
1114
+ background: "#111",
1115
+ borderBottom: "1px solid #1c1c1c"
1116
+ },
1117
+ children: [
1118
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1119
+ "div",
1120
+ {
1121
+ style: {
1122
+ flex: `0 0 ${LABEL_COL_WIDTH}px`,
1123
+ minWidth: 0,
1124
+ boxSizing: "border-box",
1125
+ position: "sticky",
1126
+ left: 0,
1127
+ zIndex: 5,
1128
+ display: "flex",
1129
+ flexDirection: "column",
1130
+ justifyContent: "center",
1131
+ gap: "2px",
1132
+ padding: "0 8px",
1133
+ borderRight: "1px solid #1c1c1c",
1134
+ background: "#0f0f0f"
1135
+ },
1136
+ children: [
1137
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#9a9a9a" }, children: t.fpsLabel }),
1138
+ stats && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "data-fps-min": Math.round(stats.minFps), style: { color, fontSize: "10px" }, children: t.fpsBadge(Math.round(stats.minFps), stats.droppedFrames) })
1139
+ ]
1140
+ }
1141
+ ),
1142
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1143
+ "div",
1144
+ {
1145
+ style: {
1146
+ flex: scrolls ? "0 0 auto" : 1,
1147
+ width: scrolls ? `${trackWidth}px` : void 0,
1148
+ position: "relative",
1149
+ padding: `0 ${TRACK_PAD_X}px`
1150
+ },
1151
+ children: stats && paths ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_preact3.Fragment, { children: [
1152
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1153
+ "svg",
1154
+ {
1155
+ width: "100%",
1156
+ height: HEAP_STRIP_H,
1157
+ viewBox: `0 0 ${HEAP_VB_W} ${HEAP_VB_H}`,
1158
+ preserveAspectRatio: "none",
1159
+ style: { display: "block" },
1160
+ "aria-hidden": "true",
1161
+ children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: paths.area, fill: color, fillOpacity: 0.16 }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: paths.line, fill: "none", stroke: color, strokeWidth: 1.2, vectorEffect: "non-scaling-stroke" })
1164
+ ]
1165
+ }
1166
+ ),
1167
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1168
+ "span",
1169
+ {
1170
+ style: { position: "absolute", top: "2px", right: "6px", color: "#666", fontSize: "10px", pointerEvents: "none" },
1171
+ children: t.fpsWorst(Math.round(stats.longestFrameMs))
1172
+ }
1173
+ )
1174
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1175
+ "span",
1176
+ {
1177
+ style: { position: "absolute", top: "50%", left: "8px", transform: "translateY(-50%)", color: "#666", fontSize: "10px" },
1178
+ children: t.fpsUnsupported
1179
+ }
1180
+ )
1181
+ }
1182
+ )
1183
+ ]
1184
+ }
1185
+ );
1186
+ }
1187
+ function Timeline({ signals, duration, startedAt, heapSamples, frames, onJump }) {
1188
+ const { t } = useI18n();
1189
+ const trackRef = (0, import_hooks2.useRef)(null);
1190
+ const containerRef = (0, import_hooks2.useRef)(null);
1191
+ const contentRef = (0, import_hooks2.useRef)(null);
1192
+ const [cursorLeftPx, setCursorLeftPx] = (0, import_hooks2.useState)(null);
1193
+ const [hovered, setHovered] = (0, import_hooks2.useState)(null);
1194
+ const [cursorFrac, setCursorFrac] = (0, import_hooks2.useState)(null);
1195
+ const tipRef = (0, import_hooks2.useRef)(null);
1196
+ const [tipPos, setTipPos] = (0, import_hooks2.useState)(null);
1197
+ const [viewportTrackWidth, setViewportTrackWidth] = (0, import_hooks2.useState)(0);
1198
+ (0, import_hooks2.useLayoutEffect)(() => {
1199
+ const el = containerRef.current;
1200
+ if (!el) return;
1201
+ const measure = () => setViewportTrackWidth(Math.max(0, el.clientWidth - LABEL_COL_WIDTH));
1202
+ measure();
1203
+ const ro = new ResizeObserver(measure);
1204
+ ro.observe(el);
1205
+ return () => ro.disconnect();
1206
+ }, []);
1207
+ (0, import_hooks2.useLayoutEffect)(() => {
1208
+ if (!hovered) {
1209
+ setTipPos(null);
1210
+ return;
1211
+ }
1212
+ const el = tipRef.current;
1213
+ if (!el) return;
1214
+ const r = el.getBoundingClientRect();
1215
+ const vw = window.innerWidth;
1216
+ const vh = window.innerHeight;
1217
+ const gap = 10;
1218
+ let left = hovered.clientX + gap;
1219
+ if (left + r.width + 8 > vw) left = hovered.clientX - r.width - gap;
1220
+ if (left < 8) left = Math.max(8, Math.min(vw - r.width - 8, hovered.clientX - r.width / 2));
1221
+ let top = hovered.clientY - r.height - 6;
1222
+ if (top < 8) top = hovered.clientY + 18;
1223
+ if (top + r.height + 8 > vh) top = Math.max(8, vh - r.height - 8);
1224
+ setTipPos({ left, top });
1225
+ }, [hovered]);
1226
+ const presentLanes = (0, import_hooks2.useMemo)(
1227
+ () => LANE_ORDER.filter((k) => signals.some((s) => s.kind === k)),
1228
+ [signals]
1229
+ );
1230
+ const winStart = 0;
1231
+ const winEnd = Math.max(duration, 1);
1232
+ const safeDur = winEnd;
1233
+ const ticks = (0, import_hooks2.useMemo)(() => niceTicks(winStart, winEnd), [winStart, winEnd]);
1234
+ const neededTrackWidth = safeDur / 1e3 * MIN_PX_PER_SEC;
1235
+ const trackWidth = Math.max(viewportTrackWidth, neededTrackWidth);
1236
+ const scrolls = viewportTrackWidth > 0 && trackWidth > viewportTrackWidth + 0.5;
1237
+ const hasHeap = !!heapSamples && heapSamples.length > 0;
1238
+ const hasFrames = !!frames && frames.length > 0;
1239
+ if (presentLanes.length === 0 && !hasHeap && !hasFrames) {
1240
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#888", padding: "20px", textAlign: "center", fontSize: "11px" }, children: t.noTimeBound });
1241
+ }
1242
+ function handleMouseMove(e) {
1243
+ const track = trackRef.current;
1244
+ const content = contentRef.current;
1245
+ if (!track || !content) return;
1246
+ const rect = track.getBoundingClientRect();
1247
+ const inner = rect.width - 2 * TRACK_PAD_X;
1248
+ const frac = Math.max(0, Math.min(1, (e.clientX - rect.left - TRACK_PAD_X) / inner));
1249
+ setCursorFrac(frac);
1250
+ const contLeft = content.getBoundingClientRect().left;
1251
+ setCursorLeftPx(rect.left - contLeft + TRACK_PAD_X + inner * frac);
1252
+ }
1253
+ function handleMouseLeave() {
1254
+ setCursorFrac(null);
1255
+ setCursorLeftPx(null);
1256
+ setHovered(null);
1257
+ }
1258
+ const cursorTime = cursorFrac != null ? winStart + cursorFrac * safeDur : null;
1259
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1260
+ "div",
1261
+ {
1262
+ style: {
1263
+ userSelect: "none",
1264
+ fontFamily: "SF Mono, Menlo, Consolas, monospace",
1265
+ fontSize: "11px"
1266
+ },
1267
+ children: [
1268
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1269
+ "div",
1270
+ {
1271
+ ref: containerRef,
1272
+ onMouseMove: handleMouseMove,
1273
+ onMouseLeave: handleMouseLeave,
1274
+ style: {
1275
+ position: "relative",
1276
+ overflowX: scrolls ? "auto" : "visible",
1277
+ // overflow-x:auto forces overflow-y to auto too, which would clip the
1278
+ // cursor time label sitting at top:-14px — reserve that space.
1279
+ paddingTop: scrolls ? "14px" : 0
1280
+ },
1281
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1282
+ "div",
1283
+ {
1284
+ ref: contentRef,
1285
+ style: {
1286
+ position: "relative",
1287
+ width: scrolls ? `${LABEL_COL_WIDTH + trackWidth}px` : "100%"
1288
+ },
1289
+ children: [
1290
+ cursorFrac != null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1291
+ "div",
1292
+ {
1293
+ "aria-hidden": "true",
1294
+ style: {
1295
+ position: "absolute",
1296
+ top: 0,
1297
+ bottom: 0,
1298
+ left: `${cursorLeftPx ?? 0}px`,
1299
+ width: "1px",
1300
+ background: "#3b82f680",
1301
+ pointerEvents: "none",
1302
+ zIndex: 1
1303
+ }
1304
+ }
1305
+ ),
1306
+ cursorFrac != null && cursorTime != null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1307
+ "div",
1308
+ {
1309
+ "aria-hidden": "true",
1310
+ style: {
1311
+ position: "absolute",
1312
+ top: "-14px",
1313
+ left: `${cursorLeftPx ?? 0}px`,
1314
+ transform: cursorFrac < 0.06 ? "translateX(0)" : cursorFrac > 0.94 ? "translateX(-100%)" : "translateX(-50%)",
1315
+ fontSize: "10px",
1316
+ color: "#3b82f6",
1317
+ background: "#0d0d0d",
1318
+ padding: "0 4px",
1319
+ borderRadius: "3px",
1320
+ pointerEvents: "none",
1321
+ whiteSpace: "nowrap",
1322
+ zIndex: 2
1323
+ },
1324
+ children: formatTime(cursorTime)
1325
+ }
1326
+ ),
1327
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1328
+ HeapStrip,
1329
+ {
1330
+ samples: heapSamples,
1331
+ startedAt,
1332
+ safeDur,
1333
+ trackWidth,
1334
+ scrolls
1335
+ }
1336
+ ),
1337
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1338
+ FpsStrip,
1339
+ {
1340
+ frames,
1341
+ startedAt,
1342
+ safeDur,
1343
+ trackWidth,
1344
+ scrolls
1345
+ }
1346
+ ),
1347
+ presentLanes.map((kind, li) => {
1348
+ const items = plotLane(signals, kind, winStart, winEnd, startedAt);
1349
+ const { bars, clusters } = clusterLane(items, CLUSTER_BIN_MS / safeDur);
1350
+ const altBg = li % 2 === 0 ? "#131313" : "#161616";
1351
+ const isNet = kind === "network";
1352
+ const trackInner = Math.max(1, trackWidth - 2 * TRACK_PAD_X);
1353
+ let laneBars = bars;
1354
+ let barRows = [];
1355
+ let laneHeight = LANE_HEIGHT;
1356
+ if (isNet && bars.length > 0) {
1357
+ laneBars = [...bars].sort((a, b) => a.startFrac - b.startFrac);
1358
+ const packed = packRows(laneBars, trackInner);
1359
+ barRows = packed.rows;
1360
+ laneHeight = Math.max(LANE_HEIGHT, packed.rowCount * (NET_BAR_H + NET_ROW_GAP) + NET_ROW_GAP);
1361
+ }
1362
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1363
+ "div",
1364
+ {
1365
+ "data-lane": kind,
1366
+ style: {
1367
+ display: "flex",
1368
+ alignItems: "stretch",
1369
+ height: `${laneHeight}px`,
1370
+ background: altBg,
1371
+ borderBottom: "1px solid #1c1c1c"
1372
+ },
1373
+ children: [
1374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1375
+ "div",
1376
+ {
1377
+ style: {
1378
+ flex: `0 0 ${LABEL_COL_WIDTH}px`,
1379
+ minWidth: 0,
1380
+ boxSizing: "border-box",
1381
+ position: "sticky",
1382
+ left: 0,
1383
+ zIndex: 5,
1384
+ display: "flex",
1385
+ alignItems: "center",
1386
+ padding: "0 8px",
1387
+ color: "#9a9a9a",
1388
+ borderRight: "1px solid #1c1c1c",
1389
+ background: "#0f0f0f"
1390
+ },
1391
+ children: [
1392
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: t.kindLabel(kind) }),
1393
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#555", fontSize: "10px", marginLeft: "4px" }, children: items.length })
1394
+ ]
1395
+ }
1396
+ ),
1397
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1398
+ "div",
1399
+ {
1400
+ ref: li === 0 ? trackRef : void 0,
1401
+ style: {
1402
+ flex: scrolls ? "0 0 auto" : 1,
1403
+ width: scrolls ? `${trackWidth}px` : void 0,
1404
+ position: "relative",
1405
+ padding: `0 ${TRACK_PAD_X}px`
1406
+ },
1407
+ children: [
1408
+ ticks.map((t2, ti) => {
1409
+ const leftPct = (t2 - winStart) / safeDur * 100;
1410
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1411
+ "div",
1412
+ {
1413
+ style: {
1414
+ position: "absolute",
1415
+ top: 0,
1416
+ bottom: 0,
1417
+ left: `calc(${TRACK_PAD_X}px + (100% - ${2 * TRACK_PAD_X}px) * ${leftPct / 100})`,
1418
+ width: "1px",
1419
+ background: "#1f1f1f"
1420
+ }
1421
+ },
1422
+ `tg-${ti}`
1423
+ );
1424
+ }),
1425
+ laneBars.map((p, i) => {
1426
+ const sev = severityForSignal(p.s);
1427
+ const color = SEVERITY_OVERLAY_COLOR[sev];
1428
+ const isHot = hovered?.s === p.s;
1429
+ const baseOpacity = sev === "low" ? 0.85 : sev === "medium" ? 0.95 : 1;
1430
+ const glow = sev === "high" ? `0 0 8px ${color}cc, 0 0 0 1px ${color}` : `0 0 0 1px ${color}`;
1431
+ const row = isNet ? barRows[i] ?? 0 : 0;
1432
+ const barTop = isNet ? `${NET_ROW_GAP + row * (NET_BAR_H + NET_ROW_GAP)}px` : "6px";
1433
+ const netBrightness = isNet ? NET_ROW_BRIGHTNESS[row % NET_ROW_BRIGHTNESS.length] : 1;
1434
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1435
+ "div",
1436
+ {
1437
+ "data-bar": kind,
1438
+ onMouseEnter: (e) => {
1439
+ const r = e.currentTarget.getBoundingClientRect();
1440
+ setHovered({ s: p.s, clientX: r.left + r.width / 2, clientY: r.top });
1441
+ },
1442
+ onMouseLeave: () => setHovered(null),
1443
+ onClick: () => onJump?.(p.s),
1444
+ title: formatTime((signalAbsoluteTime(p.s) ?? startedAt) - startedAt),
1445
+ style: {
1446
+ position: "absolute",
1447
+ top: barTop,
1448
+ bottom: isNet ? void 0 : "6px",
1449
+ height: isNet ? `${NET_BAR_H}px` : void 0,
1450
+ left: `clamp(${TRACK_PAD_X}px, calc(${TRACK_PAD_X}px + (100% - ${2 * TRACK_PAD_X}px) * ${p.startFrac.toFixed(4)}), calc(100% - ${MIN_BAR_W + TRACK_PAD_X}px))`,
1451
+ width: `min(calc(100% - ${2 * TRACK_PAD_X}px), max(${MIN_BAR_W}px, calc((100% - ${2 * TRACK_PAD_X}px) * ${p.widthFrac.toFixed(4)})))`,
1452
+ background: `linear-gradient(180deg, ${color}, ${color}cc)`,
1453
+ opacity: isHot ? 1 : baseOpacity,
1454
+ borderRadius: "3px",
1455
+ boxShadow: isHot ? `0 0 10px ${color}, 0 0 0 1px #fff6` : glow,
1456
+ cursor: onJump ? "pointer" : "default",
1457
+ transition: "transform 90ms ease, box-shadow 90ms ease",
1458
+ transform: isHot ? "scaleY(1.18)" : "scaleY(1)",
1459
+ filter: isNet ? `brightness(${netBrightness})` : void 0,
1460
+ zIndex: isHot ? 3 : 2
1461
+ }
1462
+ },
1463
+ `bar-${kind}-${i}`
1464
+ );
1465
+ }),
1466
+ clusters.map((c, i) => {
1467
+ const color = SEVERITY_OVERLAY_COLOR[c.worst];
1468
+ const leftPct = c.startFrac * 100;
1469
+ const isHot = hovered?.s === c.rep;
1470
+ const baseOpacity = c.worst === "low" ? 0.85 : c.worst === "medium" ? 0.95 : 1;
1471
+ const d = MARKER_D + Math.min(6, Math.log2(c.count) * 1.6);
1472
+ const glow = c.worst === "high" ? `0 0 8px ${color}cc, 0 0 0 1px ${color}` : `0 0 0 1px ${color}`;
1473
+ const ring = c.count > 1 ? `, inset 0 0 0 1.5px #0d0d0d` : "";
1474
+ const markerLeft = `clamp(${d / 2}px, calc(${TRACK_PAD_X}px + (100% - ${2 * TRACK_PAD_X}px) * ${c.startFrac.toFixed(4)}), calc(100% - ${d / 2}px))`;
1475
+ const badgeFlip = c.startFrac > 0.85;
1476
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_preact3.Fragment, { children: [
1477
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1478
+ "div",
1479
+ {
1480
+ "data-bar": kind,
1481
+ onMouseEnter: (e) => {
1482
+ const r = e.currentTarget.getBoundingClientRect();
1483
+ setHovered({
1484
+ s: c.rep,
1485
+ clientX: r.left + r.width / 2,
1486
+ clientY: r.top,
1487
+ count: c.count,
1488
+ members: c.members
1489
+ });
1490
+ },
1491
+ onMouseLeave: () => setHovered(null),
1492
+ onClick: () => onJump?.(c.rep),
1493
+ title: formatTime((signalAbsoluteTime(c.rep) ?? startedAt) - startedAt),
1494
+ style: {
1495
+ position: "absolute",
1496
+ top: "50%",
1497
+ left: markerLeft,
1498
+ width: `${d}px`,
1499
+ height: `${d}px`,
1500
+ marginLeft: `-${d / 2}px`,
1501
+ transform: isHot ? "translateY(-50%) scale(1.35)" : "translateY(-50%)",
1502
+ background: color,
1503
+ opacity: isHot ? 1 : baseOpacity,
1504
+ borderRadius: "50%",
1505
+ boxShadow: (isHot ? `0 0 8px ${color}, 0 0 0 1px ${color}` : glow) + ring,
1506
+ cursor: onJump ? "pointer" : "default",
1507
+ transition: "transform 90ms ease, box-shadow 90ms ease",
1508
+ zIndex: isHot ? 3 : 2
1509
+ }
1510
+ }
1511
+ ),
1512
+ c.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1513
+ "span",
1514
+ {
1515
+ "aria-hidden": "true",
1516
+ style: {
1517
+ position: "absolute",
1518
+ top: "50%",
1519
+ left: markerLeft,
1520
+ marginLeft: badgeFlip ? `-${d / 2 + 3}px` : `${d / 2 + 3}px`,
1521
+ transform: badgeFlip ? "translate(-100%, -50%)" : "translateY(-50%)",
1522
+ fontSize: "9px",
1523
+ fontWeight: 600,
1524
+ lineHeight: 1,
1525
+ color: "#e8e8e8",
1526
+ background: "#0d0d0dcc",
1527
+ padding: "1px 3px",
1528
+ borderRadius: "3px",
1529
+ whiteSpace: "nowrap",
1530
+ pointerEvents: "none",
1531
+ zIndex: isHot ? 4 : 3
1532
+ },
1533
+ children: [
1534
+ "\xD7",
1535
+ c.count
1536
+ ]
1537
+ }
1538
+ )
1539
+ ] }, `pt-${kind}-${i}`);
1540
+ })
1541
+ ]
1542
+ }
1543
+ )
1544
+ ]
1545
+ },
1546
+ kind
1547
+ );
1548
+ }),
1549
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1550
+ "div",
1551
+ {
1552
+ style: {
1553
+ display: "flex",
1554
+ height: `${AXIS_HEIGHT}px`,
1555
+ background: "#0f0f0f",
1556
+ color: "#666",
1557
+ fontSize: "10px"
1558
+ },
1559
+ children: [
1560
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1561
+ "div",
1562
+ {
1563
+ style: {
1564
+ flex: `0 0 ${LABEL_COL_WIDTH}px`,
1565
+ minWidth: 0,
1566
+ boxSizing: "border-box",
1567
+ position: "sticky",
1568
+ left: 0,
1569
+ zIndex: 5,
1570
+ display: "flex",
1571
+ alignItems: "center",
1572
+ justifyContent: "flex-end",
1573
+ padding: "0 8px",
1574
+ borderRight: "1px solid #1c1c1c",
1575
+ background: "#0f0f0f",
1576
+ color: "#555"
1577
+ },
1578
+ children: t.timeAxis
1579
+ }
1580
+ ),
1581
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1582
+ "div",
1583
+ {
1584
+ style: {
1585
+ flex: scrolls ? "0 0 auto" : 1,
1586
+ width: scrolls ? `${trackWidth}px` : void 0,
1587
+ position: "relative",
1588
+ padding: `0 ${TRACK_PAD_X}px`
1589
+ },
1590
+ children: ticks.map((t2, ti) => {
1591
+ const leftPct = (t2 - winStart) / safeDur * 100;
1592
+ const align = leftPct < 6 ? "translateX(0)" : leftPct > 94 ? "translateX(-100%)" : "translateX(-50%)";
1593
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_preact3.Fragment, { children: [
1594
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1595
+ "div",
1596
+ {
1597
+ style: {
1598
+ position: "absolute",
1599
+ top: 0,
1600
+ height: "4px",
1601
+ width: "1px",
1602
+ background: "#444",
1603
+ left: `calc(${TRACK_PAD_X}px + (100% - ${2 * TRACK_PAD_X}px) * ${leftPct / 100})`
1604
+ }
1605
+ }
1606
+ ),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1608
+ "div",
1609
+ {
1610
+ style: {
1611
+ position: "absolute",
1612
+ top: "5px",
1613
+ left: `calc(${TRACK_PAD_X}px + (100% - ${2 * TRACK_PAD_X}px) * ${leftPct / 100})`,
1614
+ transform: align,
1615
+ whiteSpace: "nowrap"
1616
+ },
1617
+ children: formatTime(t2)
1618
+ }
1619
+ )
1620
+ ] }, `tl-${ti}`);
1621
+ })
1622
+ }
1623
+ )
1624
+ ]
1625
+ }
1626
+ )
1627
+ ]
1628
+ }
1629
+ )
1630
+ }
1631
+ ),
1632
+ hovered && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1633
+ "div",
1634
+ {
1635
+ ref: tipRef,
1636
+ role: "tooltip",
1637
+ style: {
1638
+ position: "fixed",
1639
+ // Until measured, park at the origin (hidden) so the un-flipped
1640
+ // nowrap box can't overflow the viewport right edge and flash a
1641
+ // scrollbar. useLayoutEffect then flips/clamps and reveals it.
1642
+ left: `${tipPos ? tipPos.left : 0}px`,
1643
+ top: `${tipPos ? tipPos.top : 0}px`,
1644
+ visibility: tipPos ? "visible" : "hidden",
1645
+ background: "#0d0d0d",
1646
+ border: "1px solid #2a2a2a",
1647
+ borderRadius: "4px",
1648
+ padding: "4px 8px",
1649
+ fontSize: "11px",
1650
+ color: "#e6e6e6",
1651
+ pointerEvents: "none",
1652
+ zIndex: 2147483647,
1653
+ whiteSpace: "nowrap",
1654
+ boxShadow: "0 4px 12px rgba(0,0,0,0.5)"
1655
+ },
1656
+ children: hovered.s.kind === "render" && hovered.members && hovered.count && hovered.count > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1657
+ RenderClusterTip,
1658
+ {
1659
+ members: hovered.members,
1660
+ count: hovered.count,
1661
+ s: hovered.s,
1662
+ startedAt
1663
+ }
1664
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_preact3.Fragment, { children: [
1665
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TooltipContent, { s: hovered.s, startedAt }),
1666
+ hovered.count != null && hovered.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: "#888", marginLeft: "6px" }, children: [
1667
+ "\xD7",
1668
+ hovered.count
1669
+ ] })
1670
+ ] })
1671
+ }
1672
+ )
1673
+ ]
1674
+ }
1675
+ );
1676
+ }
1677
+ var BREAKDOWN_MAX = 6;
1678
+ function RenderClusterTip({
1679
+ members,
1680
+ count,
1681
+ s,
1682
+ startedAt
1683
+ }) {
1684
+ const tAbs = signalAbsoluteTime(s);
1685
+ const at = tAbs == null ? "" : `@ ${formatTime(tAbs - startedAt)}`;
1686
+ const bd = renderBreakdown(members);
1687
+ const shown = bd.slice(0, BREAKDOWN_MAX);
1688
+ const more = bd.length - shown.length;
1689
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_preact3.Fragment, { children: [
1690
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
1691
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("strong", { children: [
1692
+ count,
1693
+ " renders"
1694
+ ] }),
1695
+ " ",
1696
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#888" }, children: at })
1697
+ ] }),
1698
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { color: "#9a9a9a", marginTop: "2px" }, children: [
1699
+ shown.map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1700
+ i > 0 ? " \xB7 " : "",
1701
+ b.component,
1702
+ b.count > 1 ? ` \xD7${b.count}` : ""
1703
+ ] }, b.component)),
1704
+ more > 0 ? ` \xB7 +${more}` : ""
1705
+ ] })
1706
+ ] });
1707
+ }
1708
+ function TooltipContent({ s, startedAt }) {
1709
+ const sev = severityForSignal(s);
1710
+ const color = SEVERITY_OVERLAY_COLOR[sev];
1711
+ const tAbs = signalAbsoluteTime(s);
1712
+ const t = tAbs == null ? null : tAbs - startedAt;
1713
+ const at = t == null ? "" : `@ ${formatTime(t)}`;
1714
+ switch (s.kind) {
1715
+ case "long-task":
1716
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1717
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "long-task" }),
1718
+ " ",
1719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color }, children: [
1720
+ s.duration.toFixed(0),
1721
+ "ms"
1722
+ ] }),
1723
+ " ",
1724
+ at
1725
+ ] });
1726
+ case "forced-reflow":
1727
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1728
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "forced-reflow" }),
1729
+ " ",
1730
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color }, children: [
1731
+ s.duration.toFixed(2),
1732
+ "ms"
1733
+ ] }),
1734
+ " ",
1735
+ at
1736
+ ] });
1737
+ case "layout-shift":
1738
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1739
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "layout-shift" }),
1740
+ " ",
1741
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color }, children: s.value.toFixed(3) }),
1742
+ " ",
1743
+ at
1744
+ ] });
1745
+ case "render":
1746
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1747
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: s.component }),
1748
+ " ",
1749
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color }, children: [
1750
+ s.duration.toFixed(2),
1751
+ "ms"
1752
+ ] }),
1753
+ " ",
1754
+ at
1755
+ ] });
1756
+ case "network":
1757
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1758
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: s.url.slice(0, 40) }),
1759
+ " ",
1760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color }, children: [
1761
+ s.duration.toFixed(0),
1762
+ "ms"
1763
+ ] }),
1764
+ " ",
1765
+ at
1766
+ ] });
1767
+ case "interaction":
1768
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1769
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: s.eventType }),
1770
+ " ",
1771
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color }, children: [
1772
+ s.duration.toFixed(0),
1773
+ "ms"
1774
+ ] }),
1775
+ " ",
1776
+ at
1777
+ ] });
1778
+ case "web-vital":
1779
+ return null;
1780
+ }
1781
+ }
1782
+
1783
+ // src/render-insights.tsx
1784
+ var import_jsx_runtime4 = require("preact/jsx-runtime");
1785
+ function aggregate(signals) {
1786
+ const byName = /* @__PURE__ */ new Map();
1787
+ for (const s of signals) {
1788
+ if (!byName.has(s.component)) byName.set(s.component, []);
1789
+ byName.get(s.component).push(s);
1790
+ }
1791
+ const result = [];
1792
+ for (const [name, list] of byName) {
1793
+ let total = 0;
1794
+ let max = 0;
1795
+ let worst = "low";
1796
+ for (const s of list) {
1797
+ total += s.duration;
1798
+ if (s.duration > max) max = s.duration;
1799
+ const sev = severityForSignal(s);
1800
+ if (sev === "high" || sev === "medium" && worst === "low") worst = sev;
1801
+ }
1802
+ result.push({ component: name, count: list.length, totalMs: total, maxMs: max, worstSeverity: worst });
1803
+ }
1804
+ result.sort((a, b) => b.totalMs - a.totalMs);
1805
+ return result;
1806
+ }
1807
+ var TOP_N = 5;
1808
+ function RenderInsights({ signals, onSelect }) {
1809
+ const { t } = useI18n();
1810
+ if (signals.length === 0) return null;
1811
+ const rows = aggregate(signals);
1812
+ const top = rows.slice(0, TOP_N);
1813
+ const max = top.length > 0 ? top[0].totalMs : 1;
1814
+ const hiddenCount = rows.length - top.length;
1815
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1816
+ "div",
1817
+ {
1818
+ style: {
1819
+ marginBottom: "8px",
1820
+ padding: "8px 10px",
1821
+ background: "#141414",
1822
+ border: "1px solid #1f1f1f",
1823
+ borderRadius: "8px"
1824
+ },
1825
+ children: [
1826
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#888", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: "6px" }, children: t.topRenderers }),
1827
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "4px" }, children: [
1828
+ top.map((a) => {
1829
+ const widthPct = max > 0 ? Math.max(2, a.totalMs / max * 100) : 0;
1830
+ const color = SEVERITY_COLOR[a.worstSeverity];
1831
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1832
+ "div",
1833
+ {
1834
+ "data-component": a.component,
1835
+ onClick: () => onSelect?.(a.component),
1836
+ style: {
1837
+ display: "flex",
1838
+ alignItems: "center",
1839
+ gap: "8px",
1840
+ fontSize: "11px",
1841
+ fontFamily: "SF Mono, Menlo, Consolas, monospace",
1842
+ cursor: onSelect ? "pointer" : "default",
1843
+ padding: "3px 0"
1844
+ },
1845
+ title: t.rendererDetail(a.component, a.count, a.totalMs, a.maxMs),
1846
+ children: [
1847
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { style: { flex: "0 0 30%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: a.component }),
1848
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: "1", position: "relative", height: "14px", background: "#1f1f1f", borderRadius: "2px" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1849
+ "div",
1850
+ {
1851
+ style: {
1852
+ position: "absolute",
1853
+ left: 0,
1854
+ top: 0,
1855
+ height: "100%",
1856
+ width: `${widthPct}%`,
1857
+ background: color,
1858
+ opacity: a.worstSeverity === "low" ? 0.4 : 0.7,
1859
+ borderRadius: "2px"
1860
+ }
1861
+ }
1862
+ ) }),
1863
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { flex: "0 0 auto", color, minWidth: "56px", textAlign: "right" }, children: [
1864
+ a.totalMs.toFixed(1),
1865
+ "ms"
1866
+ ] }),
1867
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { flex: "0 0 auto", color: "#888", minWidth: "32px", textAlign: "right" }, children: [
1868
+ "\xD7",
1869
+ a.count
1870
+ ] })
1871
+ ]
1872
+ },
1873
+ a.component
1874
+ );
1875
+ }),
1876
+ hiddenCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "10px", color: "#666", marginTop: "2px" }, children: t.moreComponents(hiddenCount) })
1877
+ ] })
1878
+ ]
1879
+ }
1880
+ );
1881
+ }
1882
+
1883
+ // src/panel.tsx
1884
+ var import_jsx_runtime5 = require("preact/jsx-runtime");
1885
+ var RENDER_REASON_COLOR = {
1886
+ mount: "#5ac8fa",
1887
+ state: "#34c759",
1888
+ props: "#8e8e93",
1889
+ parent: "#ff9500"
1890
+ };
1891
+ function renderReasonLabel(t, reason) {
1892
+ switch (reason) {
1893
+ case "mount":
1894
+ return t.reasonMounted;
1895
+ case "state":
1896
+ return t.reasonState;
1897
+ case "props":
1898
+ return t.reasonProps;
1899
+ case "parent":
1900
+ return t.reasonParent;
1901
+ }
1902
+ }
1903
+ function RenderReasonTag({ reason, changedProps }) {
1904
+ const { t } = useI18n();
1905
+ const color = RENDER_REASON_COLOR[reason];
1906
+ const label = renderReasonLabel(t, reason);
1907
+ const keys = reason === "props" && changedProps && changedProps.length > 0 ? `: ${changedProps.join(", ")}` : "";
1908
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1909
+ "span",
1910
+ {
1911
+ style: {
1912
+ color,
1913
+ border: `1px solid ${color}`,
1914
+ borderRadius: "4px",
1915
+ padding: "0 5px",
1916
+ fontSize: "10px",
1917
+ fontWeight: 600,
1918
+ whiteSpace: "nowrap"
1919
+ },
1920
+ children: [
1921
+ label,
1922
+ keys
1923
+ ]
1924
+ }
1925
+ );
1926
+ }
1927
+ var KIND_ORDER = [
1928
+ "forced-reflow",
1929
+ "layout-shift",
1930
+ "long-task",
1931
+ "interaction",
1932
+ "network",
1933
+ "web-vital",
1934
+ "render"
1935
+ ];
1936
+ var POSITION_STYLES2 = {
1937
+ "bottom-right": { bottom: "16px", right: "16px" },
1938
+ "bottom-left": { bottom: "16px", left: "16px" },
1939
+ "top-right": { top: "16px", right: "16px" },
1940
+ "top-left": { top: "16px", left: "16px" }
1941
+ };
1942
+ function groupByKind(signals) {
1943
+ const acc = {
1944
+ "forced-reflow": [],
1945
+ "layout-shift": [],
1946
+ "long-task": [],
1947
+ "interaction": [],
1948
+ "network": [],
1949
+ "web-vital": [],
1950
+ "render": []
1951
+ };
1952
+ for (const s of signals) acc[s.kind].push(s);
1953
+ return acc;
1954
+ }
1955
+ var WEB_VITAL_UNIT2 = {
1956
+ LCP: "ms",
1957
+ INP: "ms",
1958
+ CLS: "",
1959
+ FCP: "ms",
1960
+ TTFB: "ms"
1961
+ };
1962
+ function RatingDot({ rating }) {
1963
+ const { t } = useI18n();
1964
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1965
+ "span",
1966
+ {
1967
+ "data-rating": rating,
1968
+ "aria-label": t.ratingLabel(rating),
1969
+ style: {
1970
+ display: "inline-block",
1971
+ width: "8px",
1972
+ height: "8px",
1973
+ borderRadius: "50%",
1974
+ background: RATING_COLOR[rating],
1975
+ marginRight: "6px",
1976
+ verticalAlign: "middle"
1977
+ }
1978
+ }
1979
+ );
1980
+ }
1981
+ function hexToRgba(hex, alpha) {
1982
+ const m = /^#([0-9a-f]{6})$/i.exec(hex);
1983
+ if (!m) return hex;
1984
+ const v = m[1];
1985
+ const r = parseInt(v.slice(0, 2), 16);
1986
+ const g = parseInt(v.slice(2, 4), 16);
1987
+ const b = parseInt(v.slice(4, 6), 16);
1988
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1989
+ }
1990
+ function formatCls(value) {
1991
+ if (value === 0) return "0";
1992
+ if (value < 1e-3) return value.toExponential(2);
1993
+ if (value < 0.01) return value.toFixed(4);
1994
+ return value.toFixed(3);
1995
+ }
1996
+ function summary(s) {
1997
+ switch (s.kind) {
1998
+ case "forced-reflow":
1999
+ return `@ ${s.at.toFixed(1)}ms \u2022 duration ${s.duration.toFixed(2)}ms`;
2000
+ case "layout-shift":
2001
+ return `@ ${s.at.toFixed(1)}ms \u2022 value ${formatCls(s.value)} \u2022 ${s.sources.length} source(s)`;
2002
+ case "long-task":
2003
+ return `@ ${s.at.toFixed(1)}ms \u2022 duration ${s.duration.toFixed(1)}ms`;
2004
+ case "interaction":
2005
+ return `${s.eventType}${s.target ? ` ${s.target}` : ""} \u2022 ${s.duration.toFixed(0)}ms`;
2006
+ case "network":
2007
+ return `${s.url.length > 60 ? s.url.slice(0, 57) + "..." : s.url} \u2022 ${s.duration.toFixed(0)}ms${s.blocking ? " \u2022 blocking" : ""}`;
2008
+ case "web-vital":
2009
+ return `${s.name}: ${s.value.toFixed(2)}`;
2010
+ case "render":
2011
+ return `${s.component} \u2022 ${s.reason} \u2022 ${s.duration.toFixed(2)}ms`;
2012
+ }
2013
+ }
2014
+ var detailLabelStyle = { color: "#888", marginRight: "6px" };
2015
+ var detailRowStyle = { padding: "2px 0", display: "flex", gap: "6px" };
2016
+ var monoStyle = {
2017
+ fontFamily: "SF Mono, Menlo, Consolas, monospace",
2018
+ fontSize: "11px"
2019
+ };
2020
+ function LayoutShiftDetail({ s }) {
2021
+ const { t } = useI18n();
2022
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2023
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2024
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.value }),
2025
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.value.toFixed(4) })
2026
+ ] }),
2027
+ s.sources.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#666" }, children: t.noSourceRects }) : s.sources.map((r, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...detailRowStyle, ...monoStyle }, children: [
2028
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: "#888" }, children: [
2029
+ t.rect,
2030
+ " ",
2031
+ i + 1
2032
+ ] }),
2033
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2034
+ "x=",
2035
+ r.x.toFixed(0),
2036
+ " y=",
2037
+ r.y.toFixed(0),
2038
+ " w=",
2039
+ r.width.toFixed(0),
2040
+ " h=",
2041
+ r.height.toFixed(0)
2042
+ ] })
2043
+ ] }, i))
2044
+ ] });
2045
+ }
2046
+ function NetworkDetail({ s }) {
2047
+ const { t } = useI18n();
2048
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2049
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...detailRowStyle, ...monoStyle, wordBreak: "break-all" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.url }) }),
2050
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2051
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.started }),
2052
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2053
+ s.startedAt.toFixed(0),
2054
+ "ms"
2055
+ ] })
2056
+ ] }),
2057
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2058
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.duration }),
2059
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2060
+ s.duration.toFixed(0),
2061
+ "ms"
2062
+ ] })
2063
+ ] }),
2064
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2065
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.size }),
2066
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2067
+ (s.size / 1024).toFixed(2),
2068
+ "KB (",
2069
+ s.size,
2070
+ " ",
2071
+ t.bytes,
2072
+ ")"
2073
+ ] })
2074
+ ] }),
2075
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2076
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.renderBlocking }),
2077
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.blocking ? t.yes : t.no })
2078
+ ] })
2079
+ ] });
2080
+ }
2081
+ function WebVitalDetail({ s }) {
2082
+ const { t } = useI18n();
2083
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2084
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2085
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.metric }),
2086
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.name })
2087
+ ] }),
2088
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2089
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.value }),
2090
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.value.toFixed(2) })
2091
+ ] })
2092
+ ] });
2093
+ }
2094
+ function RenderDetail({ s }) {
2095
+ const { t } = useI18n();
2096
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2097
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2098
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.component }),
2099
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.component })
2100
+ ] }),
2101
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2102
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.reason }),
2103
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RenderReasonTag, { reason: s.reason, changedProps: s.changedProps })
2104
+ ] }),
2105
+ s.reason === "props" && s.changedProps && s.changedProps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...detailRowStyle, ...monoStyle }, children: [
2106
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.changedProps }),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: s.changedProps.join(", ") })
2108
+ ] }),
2109
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2110
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.duration }),
2111
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2112
+ s.duration.toFixed(3),
2113
+ "ms"
2114
+ ] })
2115
+ ] }),
2116
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2117
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.at }),
2118
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2119
+ s.at.toFixed(2),
2120
+ "ms"
2121
+ ] })
2122
+ ] })
2123
+ ] });
2124
+ }
2125
+ function StackFrames({
2126
+ raw,
2127
+ resolveFrame,
2128
+ limit = 8
2129
+ }) {
2130
+ const { t } = useI18n();
2131
+ const original = raw.slice(0, limit);
2132
+ const [frames, setFrames] = (0, import_hooks3.useState)(original);
2133
+ const [resolving, setResolving] = (0, import_hooks3.useState)(false);
2134
+ (0, import_hooks3.useEffect)(() => {
2135
+ if (!resolveFrame || original.length === 0) return;
2136
+ let cancelled = false;
2137
+ setResolving(true);
2138
+ Promise.all(original.map((f) => resolveFrame(f))).then((resolved) => {
2139
+ if (!cancelled) setFrames(resolved);
2140
+ }).catch(() => {
2141
+ }).finally(() => {
2142
+ if (!cancelled) setResolving(false);
2143
+ });
2144
+ return () => {
2145
+ cancelled = true;
2146
+ };
2147
+ }, [raw]);
2148
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2149
+ resolving && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#666", fontSize: "10px", marginBottom: "4px" }, children: t.resolvingSourceMaps }),
2150
+ frames.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#666" }, children: t.noStack }) : frames.map((f, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...detailRowStyle, ...monoStyle }, children: [
2151
+ f.fnName ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: f.fnName }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#666" }, children: t.anonymous }),
2152
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: "#888" }, children: [
2153
+ f.file,
2154
+ ":",
2155
+ f.line,
2156
+ ":",
2157
+ f.col
2158
+ ] })
2159
+ ] }, i))
2160
+ ] });
2161
+ }
2162
+ function ForcedReflowDetail({
2163
+ s,
2164
+ resolveFrame
2165
+ }) {
2166
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { paddingLeft: "12px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StackFrames, { raw: s.stack, resolveFrame }) });
2167
+ }
2168
+ function HotFunctions({
2169
+ attribution,
2170
+ resolveFrame
2171
+ }) {
2172
+ const { t } = useI18n();
2173
+ const [frames, setFrames] = (0, import_hooks3.useState)(attribution.map((a) => a.frame));
2174
+ (0, import_hooks3.useEffect)(() => {
2175
+ if (!resolveFrame) return;
2176
+ let cancelled = false;
2177
+ Promise.all(attribution.map((a) => resolveFrame(a.frame))).then((resolved) => {
2178
+ if (!cancelled) setFrames(resolved);
2179
+ }).catch(() => {
2180
+ });
2181
+ return () => {
2182
+ cancelled = true;
2183
+ };
2184
+ }, [attribution]);
2185
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { marginTop: "6px" }, children: [
2186
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...detailLabelStyle, textTransform: "uppercase", fontSize: "10px", letterSpacing: "0.5px", marginBottom: "1px", color: "#ff9f0a" }, children: t.hotFunctions }),
2187
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#666", fontSize: "10px", marginBottom: "4px" }, children: t.hotFunctionsHint }),
2188
+ attribution.map((a, i) => {
2189
+ const f = frames[i] ?? a.frame;
2190
+ const pct = Math.round(a.selfRatio * 100);
2191
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...monoStyle, padding: "3px 0", borderTop: i > 0 ? "1px dashed #1f1f1f" : "none" }, children: [
2192
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
2193
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#ff9f0a", fontWeight: 600 }, children: f.fnName || /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#666" }, children: t.anonymous }) }),
2194
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: SEVERITY_COLOR.medium, marginLeft: "6px" }, children: [
2195
+ pct,
2196
+ "%"
2197
+ ] }),
2198
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: "#666", marginLeft: "6px" }, children: [
2199
+ "(",
2200
+ a.sampleCount,
2201
+ ")"
2202
+ ] })
2203
+ ] }),
2204
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { color: "#888", wordBreak: "break-all" }, children: [
2205
+ f.file,
2206
+ ":",
2207
+ f.line,
2208
+ ":",
2209
+ f.col
2210
+ ] })
2211
+ ] }, i);
2212
+ })
2213
+ ] });
2214
+ }
2215
+ function LongTaskDetail({
2216
+ s,
2217
+ resolveFrame
2218
+ }) {
2219
+ const { t } = useI18n();
2220
+ const scripts = s.scripts ? [...s.scripts].sort((a, b) => b.duration - a.duration) : [];
2221
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2222
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2223
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.started }),
2224
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2225
+ s.at.toFixed(2),
2226
+ "ms"
2227
+ ] })
2228
+ ] }),
2229
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2230
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.ended }),
2231
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2232
+ (s.at + s.duration).toFixed(2),
2233
+ "ms"
2234
+ ] })
2235
+ ] }),
2236
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2237
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.duration }),
2238
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2239
+ s.duration.toFixed(2),
2240
+ "ms"
2241
+ ] })
2242
+ ] }),
2243
+ typeof s.blockingDuration === "number" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2244
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.blockingTime }),
2245
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2246
+ s.blockingDuration.toFixed(2),
2247
+ "ms"
2248
+ ] })
2249
+ ] }),
2250
+ s.attribution !== void 0 && s.attribution.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(HotFunctions, { attribution: s.attribution, resolveFrame }),
2251
+ s.scripts !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { marginTop: "6px" }, children: [
2252
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...detailLabelStyle, textTransform: "uppercase", fontSize: "10px", letterSpacing: "0.5px", marginBottom: "4px" }, children: t.scripts }),
2253
+ scripts.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#666" }, children: t.noScripts }) : scripts.map((script, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...monoStyle, padding: "3px 0", borderTop: i > 0 ? "1px dashed #1f1f1f" : "none" }, children: [
2254
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
2255
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: script.sourceFunctionName || /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#666" }, children: t.anonymous }) }),
2256
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#5ac8fa", marginLeft: "6px" }, children: script.invokerType }),
2257
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: SEVERITY_COLOR.medium, marginLeft: "6px" }, children: [
2258
+ script.duration.toFixed(1),
2259
+ "ms"
2260
+ ] })
2261
+ ] }),
2262
+ script.sourceURL && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { color: "#888", wordBreak: "break-all" }, children: [
2263
+ script.sourceURL,
2264
+ script.charPosition >= 0 ? `@${script.charPosition}` : ""
2265
+ ] }),
2266
+ script.invoker && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { color: "#666" }, children: [
2267
+ t.invoker,
2268
+ ": ",
2269
+ script.invoker
2270
+ ] })
2271
+ ] }, i))
2272
+ ] }),
2273
+ s.scripts === void 0 && s.stack.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginTop: "4px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StackFrames, { raw: s.stack, resolveFrame, limit: 5 }) })
2274
+ ] });
2275
+ }
2276
+ var INTERACTION_PHASE_COLOR = { input: "#8e8e93", processing: "#5ac8fa", presentation: "#34c759" };
2277
+ function InteractionDetail({
2278
+ s,
2279
+ resolveFrame
2280
+ }) {
2281
+ const { t } = useI18n();
2282
+ const total = Math.max(s.duration, 1);
2283
+ const phases = [
2284
+ { label: t.inputDelay, ms: s.inputDelay, color: INTERACTION_PHASE_COLOR.input },
2285
+ { label: t.processingTime, ms: s.processing, color: INTERACTION_PHASE_COLOR.processing },
2286
+ { label: t.presentation, ms: s.presentation, color: INTERACTION_PHASE_COLOR.presentation }
2287
+ ];
2288
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { paddingLeft: "12px" }, children: [
2289
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2290
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.interactionEvent }),
2291
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2292
+ s.eventType,
2293
+ s.target ? ` \xB7 ${s.target}` : ""
2294
+ ] })
2295
+ ] }),
2296
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2297
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: detailLabelStyle, children: t.duration }),
2298
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2299
+ s.duration.toFixed(0),
2300
+ "ms"
2301
+ ] })
2302
+ ] }),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "flex", height: "10px", borderRadius: "3px", overflow: "hidden", margin: "5px 0 7px", background: "#1a1a1a" }, children: phases.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { width: `${p.ms / total * 100}%`, background: p.color } }, i)) }),
2304
+ phases.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: detailRowStyle, children: [
2305
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { ...detailLabelStyle, display: "inline-flex", alignItems: "center", gap: "5px" }, children: [
2306
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { width: "7px", height: "7px", borderRadius: "2px", background: p.color, flex: "0 0 7px" } }),
2307
+ p.label
2308
+ ] }),
2309
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2310
+ p.ms.toFixed(0),
2311
+ "ms"
2312
+ ] })
2313
+ ] }, i)),
2314
+ s.attribution !== void 0 && s.attribution.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(HotFunctions, { attribution: s.attribution, resolveFrame })
2315
+ ] });
2316
+ }
2317
+ function SignalDetail({
2318
+ s,
2319
+ resolveFrame
2320
+ }) {
2321
+ switch (s.kind) {
2322
+ case "forced-reflow":
2323
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ForcedReflowDetail, { s, resolveFrame });
2324
+ case "layout-shift":
2325
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LayoutShiftDetail, { s });
2326
+ case "long-task":
2327
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LongTaskDetail, { s, resolveFrame });
2328
+ case "interaction":
2329
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InteractionDetail, { s, resolveFrame });
2330
+ case "network":
2331
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NetworkDetail, { s });
2332
+ case "web-vital":
2333
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WebVitalDetail, { s });
2334
+ case "render":
2335
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RenderDetail, { s });
2336
+ }
2337
+ }
2338
+ function SeverityDot({ sev, title }) {
2339
+ const { t } = useI18n();
2340
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2341
+ "span",
2342
+ {
2343
+ "data-severity": sev,
2344
+ "aria-label": t.severityLabel(sev),
2345
+ title: title ?? sev,
2346
+ style: {
2347
+ display: "inline-block",
2348
+ width: "8px",
2349
+ height: "8px",
2350
+ borderRadius: "50%",
2351
+ background: SEVERITY_COLOR[sev],
2352
+ flex: "0 0 8px",
2353
+ boxShadow: sev === "high" ? "0 0 6px rgba(255,59,48,0.6)" : "none"
2354
+ }
2355
+ }
2356
+ );
2357
+ }
2358
+ function SummaryLine({ signal }) {
2359
+ const { t } = useI18n();
2360
+ if (signal.kind === "web-vital") {
2361
+ const rating = webVitalRating(signal.name, signal.value);
2362
+ const unit = WEB_VITAL_UNIT2[signal.name];
2363
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2364
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RatingDot, { rating }),
2365
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: signal.name }),
2366
+ ": ",
2367
+ signal.value.toFixed(2),
2368
+ unit
2369
+ ] });
2370
+ }
2371
+ const sev = severityForSignal(signal);
2372
+ const color = SEVERITY_COLOR[sev];
2373
+ if (signal.kind === "long-task") {
2374
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2375
+ "@ ",
2376
+ signal.at.toFixed(1),
2377
+ "ms \u2022 duration",
2378
+ " ",
2379
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
2380
+ signal.duration.toFixed(1),
2381
+ "ms"
2382
+ ] })
2383
+ ] });
2384
+ }
2385
+ if (signal.kind === "forced-reflow") {
2386
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2387
+ "@ ",
2388
+ signal.at.toFixed(1),
2389
+ "ms \u2022 duration",
2390
+ " ",
2391
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
2392
+ signal.duration.toFixed(2),
2393
+ "ms"
2394
+ ] })
2395
+ ] });
2396
+ }
2397
+ if (signal.kind === "layout-shift") {
2398
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2399
+ "@ ",
2400
+ signal.at.toFixed(1),
2401
+ "ms \u2022 value",
2402
+ " ",
2403
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color }, children: formatCls(signal.value) }),
2404
+ " \u2022 ",
2405
+ t.sourceCount(signal.sources.length)
2406
+ ] });
2407
+ }
2408
+ if (signal.kind === "render") {
2409
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
2410
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: signal.component }),
2411
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RenderReasonTag, { reason: signal.reason, changedProps: signal.changedProps }),
2412
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
2413
+ signal.duration.toFixed(2),
2414
+ "ms"
2415
+ ] })
2416
+ ] });
2417
+ }
2418
+ if (signal.kind === "network") {
2419
+ const url = signal.url.length > 60 ? signal.url.slice(0, 57) + "..." : signal.url;
2420
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2421
+ url,
2422
+ " \u2022 ",
2423
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color }, children: [
2424
+ signal.duration.toFixed(0),
2425
+ "ms"
2426
+ ] }),
2427
+ signal.blocking && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: SEVERITY_COLOR.high }, children: [
2428
+ " \u2022 ",
2429
+ t.blocking
2430
+ ] })
2431
+ ] });
2432
+ }
2433
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: summary(signal) });
2434
+ }
2435
+ function cascadeRootOf(list) {
2436
+ return list.find((s) => s.reason === "state") ?? list.find((s) => s.reason === "mount") ?? [...list].sort((a, b) => a.depth - b.depth)[0];
2437
+ }
2438
+ function tabSupportsGrouping(kind) {
2439
+ return kind === "render" || kind === "forced-reflow";
2440
+ }
2441
+ function sortSignalsBySeverity(signals) {
2442
+ return [...signals].sort((a, b) => {
2443
+ const ra = severityRank(severityForSignal(a));
2444
+ const rb = severityRank(severityForSignal(b));
2445
+ if (rb !== ra) return rb - ra;
2446
+ return a.kind === "web-vital" || b.kind === "web-vital" ? 0 : ("at" in a ? a.at : 0) - ("at" in b ? b.at : 0);
2447
+ });
2448
+ }
2449
+ function downloadRecording(result) {
2450
+ const exportable = {
2451
+ ...result,
2452
+ signals: result.signals.map((s) => {
2453
+ if (s.kind === "forced-reflow" || s.kind === "long-task") {
2454
+ return { ...s, stack: s.stack };
2455
+ }
2456
+ return s;
2457
+ })
2458
+ };
2459
+ const json = JSON.stringify(exportable, null, 2);
2460
+ const blob = new Blob([json], { type: "application/json" });
2461
+ const url = URL.createObjectURL(blob);
2462
+ try {
2463
+ const a = document.createElement("a");
2464
+ a.href = url;
2465
+ a.download = `react-perfscope-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2466
+ document.body.appendChild(a);
2467
+ a.click();
2468
+ document.body.removeChild(a);
2469
+ } finally {
2470
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2471
+ }
2472
+ }
2473
+ function groupSignals(signals, mode, kind) {
2474
+ if (mode === "chronological") {
2475
+ return signals.map((s, i) => ({ label: `#${i + 1}`, count: 1, signals: [s] }));
2476
+ }
2477
+ if (mode === "component" && kind === "render") {
2478
+ const byName = /* @__PURE__ */ new Map();
2479
+ for (const s of signals) {
2480
+ if (s.kind !== "render") continue;
2481
+ const k = s.component;
2482
+ if (!byName.has(k)) byName.set(k, []);
2483
+ byName.get(k).push(s);
2484
+ }
2485
+ return Array.from(byName.entries()).sort((a, b) => b[1].length - a[1].length).map(([label, list]) => ({ label, count: list.length, signals: list }));
2486
+ }
2487
+ if (mode === "commit" && kind === "render") {
2488
+ const byCommit = /* @__PURE__ */ new Map();
2489
+ for (const s of signals) {
2490
+ if (s.kind !== "render") continue;
2491
+ if (!byCommit.has(s.commitId)) byCommit.set(s.commitId, []);
2492
+ byCommit.get(s.commitId).push(s);
2493
+ }
2494
+ return Array.from(byCommit.entries()).sort((a, b) => a[0] - b[0]).map(([id, list]) => {
2495
+ const root = cascadeRootOf(list);
2496
+ return { label: root?.component ?? `commit ${id}`, count: list.length, signals: list };
2497
+ });
2498
+ }
2499
+ if (mode === "source" && kind === "forced-reflow") {
2500
+ const bySource = /* @__PURE__ */ new Map();
2501
+ for (const s of signals) {
2502
+ if (s.kind !== "forced-reflow") continue;
2503
+ const top = s.stack[0];
2504
+ const k = top ? `${top.file}:${top.line}:${top.col}` : "(no stack)";
2505
+ if (!bySource.has(k)) bySource.set(k, []);
2506
+ bySource.get(k).push(s);
2507
+ }
2508
+ return Array.from(bySource.entries()).sort((a, b) => b[1].length - a[1].length).map(([label, list]) => ({ label, count: list.length, signals: list }));
2509
+ }
2510
+ return signals.map((s, i) => ({ label: `#${i + 1}`, count: 1, signals: [s] }));
2511
+ }
2512
+ function SignalRow({ signal, expanded, onToggleExpand, onHoverGeometry, resolveFrame }) {
2513
+ const hasGeometry = signal.kind === "layout-shift" && signal.sources.length > 0;
2514
+ const sev = severityForSignal(signal);
2515
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2516
+ "li",
2517
+ {
2518
+ "aria-expanded": expanded,
2519
+ "data-severity": sev,
2520
+ onClick: onToggleExpand,
2521
+ onMouseEnter: () => {
2522
+ if (hasGeometry && signal.kind === "layout-shift") onHoverGeometry(signal);
2523
+ },
2524
+ onMouseLeave: () => {
2525
+ if (hasGeometry) onHoverGeometry(null);
2526
+ },
2527
+ style: {
2528
+ padding: "6px 8px",
2529
+ borderTop: "1px solid #1a1a1a",
2530
+ borderLeft: `3px solid ${sev === "low" ? "transparent" : SEVERITY_COLOR[sev]}`,
2531
+ fontFamily: "SF Mono, Menlo, Consolas, monospace",
2532
+ fontSize: "11px",
2533
+ cursor: "pointer",
2534
+ userSelect: "none"
2535
+ },
2536
+ children: [
2537
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px" }, children: [
2538
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#888", width: "10px" }, children: expanded ? "\u25BC" : "\u25B6" }),
2539
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SeverityDot, { sev }),
2540
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SummaryLine, { signal })
2541
+ ] }),
2542
+ expanded && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginTop: "6px", paddingTop: "6px", borderTop: "1px dashed #2a2a2a" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SignalDetail, { s: signal, resolveFrame }) })
2543
+ ]
2544
+ }
2545
+ );
2546
+ }
2547
+ function CascadeMembers({ members }) {
2548
+ const minDepth = members.reduce((m, s) => Math.min(m, s.depth), Infinity);
2549
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: members.map((m, i) => {
2550
+ const indent = (m.depth - minDepth) * 14;
2551
+ const isCascade = m.reason === "parent";
2552
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2553
+ "div",
2554
+ {
2555
+ style: {
2556
+ marginLeft: `${indent}px`,
2557
+ padding: "2px 0",
2558
+ display: "flex",
2559
+ alignItems: "center",
2560
+ gap: "6px"
2561
+ },
2562
+ children: [
2563
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: isCascade ? RENDER_REASON_COLOR.parent : "#555" }, children: m.depth > minDepth ? "\u2514" : "\u2022" }),
2564
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: m.component }),
2565
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RenderReasonTag, { reason: m.reason, changedProps: m.changedProps }),
2566
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: "#888" }, children: [
2567
+ m.duration.toFixed(2),
2568
+ "ms"
2569
+ ] })
2570
+ ]
2571
+ },
2572
+ i
2573
+ );
2574
+ }) });
2575
+ }
2576
+ function LanguageToggle() {
2577
+ const { lang, setLang, t } = useI18n();
2578
+ const langs = ["en", "ko"];
2579
+ const labels = { en: "EN", ko: "\uD55C" };
2580
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2581
+ "div",
2582
+ {
2583
+ role: "group",
2584
+ "aria-label": t.language,
2585
+ style: {
2586
+ display: "inline-flex",
2587
+ border: "1px solid #2a2a2a",
2588
+ borderRadius: "4px",
2589
+ overflow: "hidden"
2590
+ },
2591
+ children: langs.map((l) => {
2592
+ const active = lang === l;
2593
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2594
+ "button",
2595
+ {
2596
+ type: "button",
2597
+ "aria-pressed": active,
2598
+ "data-lang": l,
2599
+ onClick: () => setLang(l),
2600
+ style: {
2601
+ background: active ? "#2a2a2a" : "transparent",
2602
+ color: active ? "#e6e6e6" : "#888",
2603
+ border: "none",
2604
+ cursor: "pointer",
2605
+ fontSize: "11px",
2606
+ padding: "2px 8px",
2607
+ fontWeight: active ? 600 : 400
2608
+ },
2609
+ children: labels[l]
2610
+ },
2611
+ l
2612
+ );
2613
+ })
2614
+ }
2615
+ );
2616
+ }
2617
+ function Panel(props) {
2618
+ const { result, onClose, position = "bottom-right", resolveFrame } = props;
2619
+ const { t } = useI18n();
2620
+ const grouped = (0, import_hooks3.useMemo)(() => groupByKind(result.signals), [result.signals]);
2621
+ const kindsPresent = KIND_ORDER.filter((k) => grouped[k].length > 0);
2622
+ const hasTimelineSignals = result.signals.some((s) => s.kind !== "web-vital") || (result.heapSamples?.length ?? 0) > 0 || (result.frames?.length ?? 0) > 0;
2623
+ const [activeTab, setActiveTab] = (0, import_hooks3.useState)(
2624
+ kindsPresent[0] ?? "forced-reflow"
2625
+ );
2626
+ const activeKind = activeTab === "timeline" ? null : activeTab;
2627
+ const setActiveKind = (k) => setActiveTab(k);
2628
+ const [expandedKey, setExpandedKey] = (0, import_hooks3.useState)(null);
2629
+ const [groupMode, setGroupMode] = (0, import_hooks3.useState)({});
2630
+ const [sortMode, setSortMode] = (0, import_hooks3.useState)({});
2631
+ const activeOverlayCount = (0, import_hooks3.useRef)(0);
2632
+ (0, import_hooks3.useEffect)(() => () => hideAllOverlays(), []);
2633
+ function handleHover(signal) {
2634
+ if (!signal) {
2635
+ for (let i = 0; i < activeOverlayCount.current; i++) {
2636
+ hideOverlay(`signal-${i}`);
2637
+ hideOverlay(`signal-prev-${i}`);
2638
+ hideArrow(`signal-arrow-${i}`);
2639
+ }
2640
+ activeOverlayCount.current = 0;
2641
+ return;
2642
+ }
2643
+ const sev = severityForSignal(signal);
2644
+ const color = SEVERITY_OVERLAY_COLOR[sev];
2645
+ const fillAlpha = sev === "high" ? 0.2 : sev === "medium" ? 0.16 : 0.14;
2646
+ const fillRgba = hexToRgba(color, fillAlpha);
2647
+ signal.sources.forEach((r, i) => {
2648
+ if (r.width <= 0 || r.height <= 0) return;
2649
+ showOverlay(`signal-${i}`, r, { border: color, fill: fillRgba });
2650
+ const prev = signal.previousSources?.[i] ?? null;
2651
+ if (prev && prev.width > 0 && prev.height > 0) {
2652
+ showOverlay(`signal-prev-${i}`, prev, {
2653
+ border: color,
2654
+ fill: "transparent",
2655
+ dashed: true
2656
+ });
2657
+ const from = { x: prev.x + prev.width / 2, y: prev.y + prev.height / 2 };
2658
+ const to = { x: r.x + r.width / 2, y: r.y + r.height / 2 };
2659
+ const dx = to.x - from.x;
2660
+ const dy = to.y - from.y;
2661
+ if (Math.hypot(dx, dy) > 8) {
2662
+ showArrow(`signal-arrow-${i}`, from, to, color);
2663
+ }
2664
+ }
2665
+ });
2666
+ activeOverlayCount.current = signal.sources.length;
2667
+ }
2668
+ const panelStyle = {
2669
+ position: "fixed",
2670
+ ...POSITION_STYLES2[position],
2671
+ width: "460px",
2672
+ maxHeight: "70vh",
2673
+ background: "#0d0d0d",
2674
+ color: "#e6e6e6",
2675
+ border: "1px solid #2a2a2a",
2676
+ borderRadius: "12px",
2677
+ padding: "12px",
2678
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2679
+ fontSize: "12px",
2680
+ zIndex: "2147483647",
2681
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
2682
+ display: "flex",
2683
+ flexDirection: "column",
2684
+ overflow: "hidden"
2685
+ };
2686
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { role: "region", "aria-label": t.panelRegion, style: panelStyle, children: [
2687
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("header", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }, children: [
2688
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "react-perfscope" }),
2689
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: "4px", alignItems: "center" }, children: [
2690
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LanguageToggle, {}),
2691
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2692
+ "button",
2693
+ {
2694
+ type: "button",
2695
+ "aria-label": t.saveAria,
2696
+ onClick: () => downloadRecording(result),
2697
+ title: t.saveTitle,
2698
+ style: {
2699
+ background: "transparent",
2700
+ color: "#e6e6e6",
2701
+ border: "1px solid #2a2a2a",
2702
+ borderRadius: "4px",
2703
+ cursor: "pointer",
2704
+ fontSize: "11px",
2705
+ padding: "2px 8px"
2706
+ },
2707
+ children: t.save
2708
+ }
2709
+ ),
2710
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2711
+ "button",
2712
+ {
2713
+ type: "button",
2714
+ "aria-label": t.closeAria,
2715
+ onClick: onClose,
2716
+ style: { background: "transparent", color: "#e6e6e6", border: "none", cursor: "pointer", fontSize: "16px" },
2717
+ children: "\xD7"
2718
+ }
2719
+ )
2720
+ ] })
2721
+ ] }),
2722
+ kindsPresent.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#888" }, children: t.noSignals }),
2723
+ kindsPresent.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2724
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2725
+ SummaryHeader,
2726
+ {
2727
+ signals: result.signals,
2728
+ grouped,
2729
+ kindsPresent,
2730
+ onKindClick: (k) => {
2731
+ setActiveTab(k);
2732
+ setExpandedKey(null);
2733
+ }
2734
+ }
2735
+ ),
2736
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("nav", { style: { display: "flex", gap: "4px", flexWrap: "wrap", marginBottom: "8px" }, children: [
2737
+ hasTimelineSignals && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2738
+ "button",
2739
+ {
2740
+ type: "button",
2741
+ "data-kind": "timeline",
2742
+ onClick: () => {
2743
+ setActiveTab("timeline");
2744
+ setExpandedKey(null);
2745
+ },
2746
+ style: {
2747
+ background: activeTab === "timeline" ? "#2a2a2a" : "#1a1a1a",
2748
+ color: "#e6e6e6",
2749
+ border: "1px solid #2a2a2a",
2750
+ borderRadius: "6px",
2751
+ padding: "4px 8px",
2752
+ cursor: "pointer",
2753
+ fontSize: "11px"
2754
+ },
2755
+ children: t.timeline
2756
+ },
2757
+ "timeline"
2758
+ ),
2759
+ kindsPresent.map((kind) => {
2760
+ const worst = worstSeverity(grouped[kind]);
2761
+ const active = activeKind === kind;
2762
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2763
+ "button",
2764
+ {
2765
+ type: "button",
2766
+ "data-kind": kind,
2767
+ "data-worst-severity": worst,
2768
+ onClick: () => {
2769
+ setActiveTab(kind);
2770
+ setExpandedKey(null);
2771
+ },
2772
+ style: {
2773
+ background: active ? "#2a2a2a" : "#1a1a1a",
2774
+ color: "#e6e6e6",
2775
+ border: `1px solid ${worst === "low" ? "#2a2a2a" : SEVERITY_COLOR[worst]}`,
2776
+ borderRadius: "6px",
2777
+ padding: "4px 8px",
2778
+ cursor: "pointer",
2779
+ fontSize: "11px",
2780
+ display: "inline-flex",
2781
+ alignItems: "center",
2782
+ gap: "6px"
2783
+ },
2784
+ children: [
2785
+ worst !== "low" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SeverityDot, { sev: worst, title: t.worstLabel(worst) }),
2786
+ t.kindLabel(kind),
2787
+ " ",
2788
+ grouped[kind].length
2789
+ ]
2790
+ },
2791
+ kind
2792
+ );
2793
+ })
2794
+ ] }),
2795
+ activeKind && activeTab !== "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "12px", marginBottom: "6px", fontSize: "11px", color: "#888", flexWrap: "wrap" }, children: [
2796
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "flex", alignItems: "center", gap: "6px" }, children: [
2797
+ t.sort,
2798
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2799
+ "select",
2800
+ {
2801
+ "aria-label": t.sort,
2802
+ value: sortMode[activeKind] ?? "chronological",
2803
+ onChange: (e) => {
2804
+ const v = e.target.value;
2805
+ setSortMode({ ...sortMode, [activeKind]: v });
2806
+ setExpandedKey(null);
2807
+ },
2808
+ style: { background: "#1a1a1a", color: "#e6e6e6", border: "1px solid #2a2a2a", borderRadius: "4px", padding: "2px 6px", fontSize: "11px" },
2809
+ children: [
2810
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "chronological", children: t.sortChronological }),
2811
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "severity", children: t.sortSeverity })
2812
+ ]
2813
+ }
2814
+ )
2815
+ ] }),
2816
+ tabSupportsGrouping(activeKind) && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { style: { display: "flex", alignItems: "center", gap: "6px" }, children: [
2817
+ t.groupBy,
2818
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2819
+ "select",
2820
+ {
2821
+ "aria-label": t.groupBy,
2822
+ value: groupMode[activeKind] ?? "chronological",
2823
+ onChange: (e) => {
2824
+ const v = e.target.value;
2825
+ setGroupMode({ ...groupMode, [activeKind]: v });
2826
+ setExpandedKey(null);
2827
+ },
2828
+ style: { background: "#1a1a1a", color: "#e6e6e6", border: "1px solid #2a2a2a", borderRadius: "4px", padding: "2px 6px", fontSize: "11px" },
2829
+ children: [
2830
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "chronological", children: t.groupChronological }),
2831
+ activeKind === "render" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "commit", children: t.groupCommit }),
2832
+ activeKind === "render" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "component", children: t.groupComponent }),
2833
+ activeKind === "forced-reflow" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "source", children: t.groupSource })
2834
+ ]
2835
+ }
2836
+ )
2837
+ ] })
2838
+ ] }),
2839
+ activeTab === "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { flexGrow: 1, overflowY: "auto", paddingTop: "4px", paddingBottom: "24px" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2840
+ Timeline,
2841
+ {
2842
+ signals: result.signals,
2843
+ duration: result.duration,
2844
+ startedAt: result.startedAt,
2845
+ heapSamples: result.heapSamples,
2846
+ frames: result.frames,
2847
+ onJump: (s) => {
2848
+ setActiveTab(s.kind);
2849
+ const inOrder = grouped[s.kind];
2850
+ const idx = inOrder.indexOf(s);
2851
+ if (idx >= 0) setExpandedKey(`${s.kind}-${idx}`);
2852
+ }
2853
+ }
2854
+ ) }),
2855
+ activeKind === "render" && activeTab !== "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2856
+ RenderInsights,
2857
+ {
2858
+ signals: grouped.render.filter((s) => s.kind === "render"),
2859
+ onSelect: () => {
2860
+ setGroupMode({ ...groupMode, render: "component" });
2861
+ setExpandedKey(null);
2862
+ }
2863
+ }
2864
+ ),
2865
+ activeKind === "interaction" && activeTab !== "timeline" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2866
+ "div",
2867
+ {
2868
+ style: {
2869
+ padding: "6px 8px",
2870
+ marginBottom: "6px",
2871
+ fontSize: "11px",
2872
+ color: "#888",
2873
+ background: "#141414",
2874
+ border: "1px solid #1f1f1f",
2875
+ borderRadius: "6px"
2876
+ },
2877
+ children: t.interactionThresholdHint
2878
+ }
2879
+ ),
2880
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0, overflowY: "auto", flexGrow: 1, display: activeTab === "timeline" ? "none" : void 0 }, children: activeKind && (() => {
2881
+ const baseSignals = (sortMode[activeKind] ?? "chronological") === "severity" ? sortSignalsBySeverity(grouped[activeKind]) : grouped[activeKind];
2882
+ return groupSignals(baseSignals, groupMode[activeKind] ?? "chronological", activeKind);
2883
+ })().map((g, gi) => {
2884
+ const currentMode = groupMode[activeKind] ?? "chronological";
2885
+ if (currentMode === "chronological") {
2886
+ const key2 = `${activeKind}-${gi}`;
2887
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2888
+ SignalRow,
2889
+ {
2890
+ signal: g.signals[0],
2891
+ expanded: expandedKey === key2,
2892
+ onToggleExpand: () => setExpandedKey(expandedKey === key2 ? null : key2),
2893
+ onHoverGeometry: handleHover,
2894
+ resolveFrame
2895
+ },
2896
+ key2
2897
+ );
2898
+ }
2899
+ const key = `${activeKind}-group-${gi}`;
2900
+ const isOpen = expandedKey === key;
2901
+ const isCascade = activeKind === "render" && currentMode === "commit";
2902
+ const renderMembers = g.signals.filter((s) => s.kind === "render");
2903
+ const unnecessary = isCascade ? renderMembers.filter((s) => s.reason === "parent").length : 0;
2904
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2905
+ "li",
2906
+ {
2907
+ "aria-expanded": isOpen,
2908
+ onClick: () => setExpandedKey(isOpen ? null : key),
2909
+ style: {
2910
+ padding: "6px 8px",
2911
+ borderTop: "1px solid #1a1a1a",
2912
+ borderLeft: unnecessary > 0 ? `3px solid ${RENDER_REASON_COLOR.parent}` : void 0,
2913
+ fontFamily: "SF Mono, Menlo, Consolas, monospace",
2914
+ fontSize: "11px",
2915
+ cursor: "pointer",
2916
+ userSelect: "none"
2917
+ },
2918
+ children: [
2919
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }, children: [
2920
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "#888", width: "10px" }, children: isOpen ? "\u25BC" : "\u25B6" }),
2921
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
2922
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: g.label }),
2923
+ " \xD7",
2924
+ g.count
2925
+ ] }),
2926
+ isCascade && unnecessary > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: RENDER_REASON_COLOR.parent, fontSize: "10px" }, children: [
2927
+ "\u26A0 ",
2928
+ t.unnecessaryRenders(unnecessary)
2929
+ ] })
2930
+ ] }),
2931
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginTop: "6px", paddingTop: "6px", borderTop: "1px dashed #2a2a2a", paddingLeft: "8px" }, children: isCascade ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CascadeMembers, { members: renderMembers }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2932
+ g.signals.slice(0, 20).map((s, si) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { padding: "2px 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SummaryLine, { signal: s }) }, si)),
2933
+ g.signals.length > 20 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { color: "#888", marginTop: "4px" }, children: t.moreItems(g.signals.length - 20) })
2934
+ ] }) })
2935
+ ]
2936
+ },
2937
+ key
2938
+ );
2939
+ }) })
2940
+ ] })
2941
+ ] });
2942
+ }
2943
+
2944
+ // src/app.tsx
2945
+ var import_jsx_runtime6 = require("preact/jsx-runtime");
2946
+ function App(props) {
2947
+ const { recorder, position = "bottom-right", resolveFrame, finalize } = props;
2948
+ const [recording, setRecording] = (0, import_hooks4.useState)(false);
2949
+ const [result, setResult] = (0, import_hooks4.useState)(null);
2950
+ const [elapsedMs, setElapsedMs] = (0, import_hooks4.useState)(0);
2951
+ const startedAtRef = (0, import_hooks4.useRef)(0);
2952
+ const rafRef = (0, import_hooks4.useRef)(null);
2953
+ const resultTokenRef = (0, import_hooks4.useRef)(0);
2954
+ (0, import_hooks4.useEffect)(() => {
2955
+ if (!recording) {
2956
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
2957
+ return;
2958
+ }
2959
+ const tick = () => {
2960
+ setElapsedMs(performance.now() - startedAtRef.current);
2961
+ rafRef.current = requestAnimationFrame(tick);
2962
+ };
2963
+ rafRef.current = requestAnimationFrame(tick);
2964
+ return () => {
2965
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
2966
+ };
2967
+ }, [recording]);
2968
+ function onToggle() {
2969
+ if (!recording) {
2970
+ startedAtRef.current = performance.now();
2971
+ setElapsedMs(0);
2972
+ setResult(null);
2973
+ recorder.start();
2974
+ setRecording(true);
2975
+ } else {
2976
+ const r = recorder.stop();
2977
+ const token = ++resultTokenRef.current;
2978
+ setRecording(false);
2979
+ setResult(r);
2980
+ if (finalize) {
2981
+ finalize(r).then((enriched) => {
2982
+ if (resultTokenRef.current === token) setResult(enriched);
2983
+ }).catch(() => {
2984
+ });
2985
+ }
2986
+ }
2987
+ }
2988
+ function onClose() {
2989
+ setResult(null);
2990
+ }
2991
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(I18nProvider, { children: [
2992
+ result === null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2993
+ Widget,
2994
+ {
2995
+ recording,
2996
+ elapsedMs,
2997
+ onToggle,
2998
+ position
2999
+ }
3000
+ ),
3001
+ result !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Panel, { result, position, onClose, resolveFrame })
3002
+ ] });
3003
+ }
3004
+
3005
+ // src/mount.tsx
3006
+ var import_jsx_runtime7 = require("preact/jsx-runtime");
3007
+ function mount(opts) {
3008
+ const { recorder, position = "bottom-right", host = document.body, resolveFrame, finalize } = opts;
3009
+ return mountShadow(
3010
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(App, { recorder, position, resolveFrame, finalize }),
3011
+ { parent: host }
3012
+ );
3013
+ }
3014
+ // Annotate the CommonJS export names for ESM import in node:
3015
+ 0 && (module.exports = {
3016
+ App,
3017
+ Panel,
3018
+ Widget,
3019
+ hideAllOverlays,
3020
+ hideOverlay,
3021
+ mount,
3022
+ mountShadow,
3023
+ showOverlay
3024
+ });
3025
+ //# sourceMappingURL=index.cjs.map