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