@react-perfscope/ui 0.7.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -90,6 +90,7 @@ function mountShadow(vnode, opts = {}) {
90
90
  }
91
91
 
92
92
  // src/app.tsx
93
+ var import_preact4 = require("preact");
93
94
  var import_hooks6 = require("preact/hooks");
94
95
 
95
96
  // src/i18n.ts
@@ -288,8 +289,8 @@ function isLang(v) {
288
289
  return v === "en" || v === "ko";
289
290
  }
290
291
  function readStoredLang() {
291
- if (typeof localStorage === "undefined") return "en";
292
292
  try {
293
+ if (typeof localStorage === "undefined") return "en";
293
294
  const v = localStorage.getItem(STORAGE_KEY);
294
295
  return isLang(v) ? v : "en";
295
296
  } catch {
@@ -306,11 +307,11 @@ function I18nProvider({ children }) {
306
307
  const [lang, setLangState] = (0, import_hooks.useState)(() => readStoredLang());
307
308
  const setLang = (0, import_hooks.useCallback)((l) => {
308
309
  setLangState(l);
309
- if (typeof localStorage !== "undefined") {
310
- try {
310
+ try {
311
+ if (typeof localStorage !== "undefined") {
311
312
  localStorage.setItem(STORAGE_KEY, l);
312
- } catch {
313
313
  }
314
+ } catch {
314
315
  }
315
316
  }, []);
316
317
  const value = (0, import_hooks.useMemo)(() => ({ lang, setLang, t: STRINGS[lang] }), [lang, setLang]);
@@ -443,12 +444,12 @@ function showArrow(id, from, to, color = DEFAULT_BORDER) {
443
444
  const minX = Math.min(from.x, to.x);
444
445
  const minY = Math.min(from.y, to.y);
445
446
  const w = Math.max(2, Math.abs(to.x - from.x));
446
- const h3 = Math.max(2, Math.abs(to.y - from.y));
447
+ const h4 = Math.max(2, Math.abs(to.y - from.y));
447
448
  svg.style.left = `${minX}px`;
448
449
  svg.style.top = `${minY}px`;
449
450
  svg.setAttribute("width", `${w}`);
450
- svg.setAttribute("height", `${h3}`);
451
- svg.setAttribute("viewBox", `0 0 ${w} ${h3}`);
451
+ svg.setAttribute("height", `${h4}`);
452
+ svg.setAttribute("viewBox", `0 0 ${w} ${h4}`);
452
453
  svg.style.opacity = "1";
453
454
  while (svg.firstChild) svg.removeChild(svg.firstChild);
454
455
  const fx = from.x - minX;
@@ -530,7 +531,7 @@ var WEB_VITAL_THRESHOLDS = {
530
531
  TTFB: [800, 1800]
531
532
  };
532
533
  function webVitalRating(name, value) {
533
- const [good, needs] = WEB_VITAL_THRESHOLDS[name];
534
+ const [good, needs] = WEB_VITAL_THRESHOLDS[name] ?? [Infinity, Infinity];
534
535
  if (value <= good) return "good";
535
536
  if (value <= needs) return "needs";
536
537
  return "poor";
@@ -591,15 +592,15 @@ var WEB_VITAL_UNIT = {
591
592
  TTFB: "ms"
592
593
  };
593
594
  function formatVitalValue(name, value) {
594
- if (name === "CLS") return value.toFixed(3);
595
+ const unit = WEB_VITAL_UNIT[name] ?? "ms";
596
+ if (unit !== "ms") return value.toFixed(3);
595
597
  if (value >= 1e3) return (value / 1e3).toFixed(2) + "s";
596
- return value.toFixed(0);
598
+ return value.toFixed(0) + "ms";
597
599
  }
598
600
  function VitalChip({ s }) {
599
601
  const { t } = useI18n();
600
602
  const rating = webVitalRating(s.name, s.value);
601
603
  const color = RATING_COLOR[rating];
602
- const unit = WEB_VITAL_UNIT[s.name];
603
604
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
604
605
  "span",
605
606
  {
@@ -631,10 +632,7 @@ function VitalChip({ s }) {
631
632
  }
632
633
  ),
633
634
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { style: { color: "#e6e6e6" }, children: s.name }),
634
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color }, children: [
635
- formatVitalValue(s.name, s.value),
636
- unit !== "s" ? unit : ""
637
- ] })
635
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color }, children: formatVitalValue(s.name, s.value) })
638
636
  ]
639
637
  }
640
638
  );
@@ -3284,6 +3282,65 @@ function Panel(props) {
3284
3282
 
3285
3283
  // src/app.tsx
3286
3284
  var import_jsx_runtime10 = require("preact/jsx-runtime");
3285
+ var PanelErrorBoundary = class extends import_preact4.Component {
3286
+ constructor() {
3287
+ super(...arguments);
3288
+ this.state = { error: null };
3289
+ }
3290
+ static getDerivedStateFromError(error) {
3291
+ return { error };
3292
+ }
3293
+ componentDidCatch(error) {
3294
+ console.warn("[react-perfscope] panel crashed:", error);
3295
+ }
3296
+ render() {
3297
+ if (this.state.error === null) return this.props.children;
3298
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
3299
+ "div",
3300
+ {
3301
+ role: "alert",
3302
+ style: {
3303
+ position: "fixed",
3304
+ bottom: "16px",
3305
+ right: "16px",
3306
+ zIndex: 2147483647,
3307
+ display: "flex",
3308
+ alignItems: "center",
3309
+ gap: "8px",
3310
+ padding: "8px 12px",
3311
+ background: "#141414",
3312
+ border: "1px solid #ff3b30",
3313
+ borderRadius: "8px",
3314
+ color: "#e6e6e6",
3315
+ fontSize: "12px",
3316
+ fontFamily: "SF Mono, Menlo, Consolas, monospace"
3317
+ },
3318
+ children: [
3319
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: "react-perfscope panel crashed \u2014 see console" }),
3320
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3321
+ "button",
3322
+ {
3323
+ "aria-label": "Close panel",
3324
+ onClick: () => {
3325
+ this.setState({ error: null });
3326
+ this.props.onClose();
3327
+ },
3328
+ style: {
3329
+ background: "none",
3330
+ border: "1px solid #444",
3331
+ borderRadius: "4px",
3332
+ color: "#e6e6e6",
3333
+ cursor: "pointer",
3334
+ padding: "2px 8px"
3335
+ },
3336
+ children: "\xD7"
3337
+ }
3338
+ )
3339
+ ]
3340
+ }
3341
+ );
3342
+ }
3343
+ };
3287
3344
  function App(props) {
3288
3345
  const { recorder, position = "bottom-right", resolveFrame, finalize } = props;
3289
3346
  const [recording, setRecording] = (0, import_hooks6.useState)(false);
@@ -3312,12 +3369,23 @@ function App(props) {
3312
3369
  startedAtRef.current = performance.now();
3313
3370
  setElapsedMs(0);
3314
3371
  setResult(null);
3315
- recorder.start();
3372
+ try {
3373
+ recorder.start();
3374
+ } catch (err) {
3375
+ console.warn("[react-perfscope] failed to start recording:", err);
3376
+ return;
3377
+ }
3316
3378
  setRecording(true);
3317
3379
  } else {
3318
- const r = recorder.stop();
3319
- const token = ++resultTokenRef.current;
3320
3380
  setRecording(false);
3381
+ let r;
3382
+ try {
3383
+ r = recorder.stop();
3384
+ } catch (err) {
3385
+ console.warn("[react-perfscope] failed to stop recording:", err);
3386
+ return;
3387
+ }
3388
+ const token = ++resultTokenRef.current;
3321
3389
  setResult(r);
3322
3390
  if (finalize) {
3323
3391
  finalize(r).then((enriched) => {
@@ -3340,14 +3408,31 @@ function App(props) {
3340
3408
  position
3341
3409
  }
3342
3410
  ),
3343
- result !== null && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Panel, { result, position, onClose, resolveFrame })
3411
+ result !== null && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PanelErrorBoundary, { onClose, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Panel, { result, position, onClose, resolveFrame }) })
3344
3412
  ] });
3345
3413
  }
3346
3414
 
3347
3415
  // src/mount.tsx
3348
3416
  var import_jsx_runtime11 = require("preact/jsx-runtime");
3349
3417
  function mount(opts) {
3350
- const { recorder, position = "bottom-right", host = document.body, resolveFrame, finalize } = opts;
3418
+ if (!opts.host && !document.body) {
3419
+ let torn = false;
3420
+ let unmount = null;
3421
+ const onReady = () => {
3422
+ if (torn || !document.body) return;
3423
+ unmount = mountNow(opts, document.body);
3424
+ };
3425
+ document.addEventListener("DOMContentLoaded", onReady, { once: true });
3426
+ return () => {
3427
+ torn = true;
3428
+ document.removeEventListener("DOMContentLoaded", onReady);
3429
+ unmount?.();
3430
+ };
3431
+ }
3432
+ return mountNow(opts, opts.host ?? document.body);
3433
+ }
3434
+ function mountNow(opts, host) {
3435
+ const { recorder, position = "bottom-right", resolveFrame, finalize } = opts;
3351
3436
  if (host.querySelector("[data-perfscope-host]")) {
3352
3437
  console.warn("[react-perfscope] mount(): a widget is already mounted here \u2014 ignoring this call");
3353
3438
  return () => {