@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.js CHANGED
@@ -57,6 +57,7 @@ function mountShadow(vnode, opts = {}) {
57
57
  }
58
58
 
59
59
  // src/app.tsx
60
+ import { Component } from "preact";
60
61
  import { useState as useState4, useRef as useRef3, useEffect as useEffect2 } from "preact/hooks";
61
62
 
62
63
  // src/i18n.ts
@@ -255,8 +256,8 @@ function isLang(v) {
255
256
  return v === "en" || v === "ko";
256
257
  }
257
258
  function readStoredLang() {
258
- if (typeof localStorage === "undefined") return "en";
259
259
  try {
260
+ if (typeof localStorage === "undefined") return "en";
260
261
  const v = localStorage.getItem(STORAGE_KEY);
261
262
  return isLang(v) ? v : "en";
262
263
  } catch {
@@ -273,11 +274,11 @@ function I18nProvider({ children }) {
273
274
  const [lang, setLangState] = useState(() => readStoredLang());
274
275
  const setLang = useCallback((l) => {
275
276
  setLangState(l);
276
- if (typeof localStorage !== "undefined") {
277
- try {
277
+ try {
278
+ if (typeof localStorage !== "undefined") {
278
279
  localStorage.setItem(STORAGE_KEY, l);
279
- } catch {
280
280
  }
281
+ } catch {
281
282
  }
282
283
  }, []);
283
284
  const value = useMemo(() => ({ lang, setLang, t: STRINGS[lang] }), [lang, setLang]);
@@ -410,12 +411,12 @@ function showArrow(id, from, to, color = DEFAULT_BORDER) {
410
411
  const minX = Math.min(from.x, to.x);
411
412
  const minY = Math.min(from.y, to.y);
412
413
  const w = Math.max(2, Math.abs(to.x - from.x));
413
- const h3 = Math.max(2, Math.abs(to.y - from.y));
414
+ const h4 = Math.max(2, Math.abs(to.y - from.y));
414
415
  svg.style.left = `${minX}px`;
415
416
  svg.style.top = `${minY}px`;
416
417
  svg.setAttribute("width", `${w}`);
417
- svg.setAttribute("height", `${h3}`);
418
- svg.setAttribute("viewBox", `0 0 ${w} ${h3}`);
418
+ svg.setAttribute("height", `${h4}`);
419
+ svg.setAttribute("viewBox", `0 0 ${w} ${h4}`);
419
420
  svg.style.opacity = "1";
420
421
  while (svg.firstChild) svg.removeChild(svg.firstChild);
421
422
  const fx = from.x - minX;
@@ -497,7 +498,7 @@ var WEB_VITAL_THRESHOLDS = {
497
498
  TTFB: [800, 1800]
498
499
  };
499
500
  function webVitalRating(name, value) {
500
- const [good, needs] = WEB_VITAL_THRESHOLDS[name];
501
+ const [good, needs] = WEB_VITAL_THRESHOLDS[name] ?? [Infinity, Infinity];
501
502
  if (value <= good) return "good";
502
503
  if (value <= needs) return "needs";
503
504
  return "poor";
@@ -558,15 +559,15 @@ var WEB_VITAL_UNIT = {
558
559
  TTFB: "ms"
559
560
  };
560
561
  function formatVitalValue(name, value) {
561
- if (name === "CLS") return value.toFixed(3);
562
+ const unit = WEB_VITAL_UNIT[name] ?? "ms";
563
+ if (unit !== "ms") return value.toFixed(3);
562
564
  if (value >= 1e3) return (value / 1e3).toFixed(2) + "s";
563
- return value.toFixed(0);
565
+ return value.toFixed(0) + "ms";
564
566
  }
565
567
  function VitalChip({ s }) {
566
568
  const { t } = useI18n();
567
569
  const rating = webVitalRating(s.name, s.value);
568
570
  const color = RATING_COLOR[rating];
569
- const unit = WEB_VITAL_UNIT[s.name];
570
571
  return /* @__PURE__ */ jsxs2(
571
572
  "span",
572
573
  {
@@ -598,10 +599,7 @@ function VitalChip({ s }) {
598
599
  }
599
600
  ),
600
601
  /* @__PURE__ */ jsx2("strong", { style: { color: "#e6e6e6" }, children: s.name }),
601
- /* @__PURE__ */ jsxs2("span", { style: { color }, children: [
602
- formatVitalValue(s.name, s.value),
603
- unit !== "s" ? unit : ""
604
- ] })
602
+ /* @__PURE__ */ jsx2("span", { style: { color }, children: formatVitalValue(s.name, s.value) })
605
603
  ]
606
604
  }
607
605
  );
@@ -3251,6 +3249,65 @@ function Panel(props) {
3251
3249
 
3252
3250
  // src/app.tsx
3253
3251
  import { jsx as jsx10, jsxs as jsxs10 } from "preact/jsx-runtime";
3252
+ var PanelErrorBoundary = class extends Component {
3253
+ constructor() {
3254
+ super(...arguments);
3255
+ this.state = { error: null };
3256
+ }
3257
+ static getDerivedStateFromError(error) {
3258
+ return { error };
3259
+ }
3260
+ componentDidCatch(error) {
3261
+ console.warn("[react-perfscope] panel crashed:", error);
3262
+ }
3263
+ render() {
3264
+ if (this.state.error === null) return this.props.children;
3265
+ return /* @__PURE__ */ jsxs10(
3266
+ "div",
3267
+ {
3268
+ role: "alert",
3269
+ style: {
3270
+ position: "fixed",
3271
+ bottom: "16px",
3272
+ right: "16px",
3273
+ zIndex: 2147483647,
3274
+ display: "flex",
3275
+ alignItems: "center",
3276
+ gap: "8px",
3277
+ padding: "8px 12px",
3278
+ background: "#141414",
3279
+ border: "1px solid #ff3b30",
3280
+ borderRadius: "8px",
3281
+ color: "#e6e6e6",
3282
+ fontSize: "12px",
3283
+ fontFamily: "SF Mono, Menlo, Consolas, monospace"
3284
+ },
3285
+ children: [
3286
+ /* @__PURE__ */ jsx10("span", { children: "react-perfscope panel crashed \u2014 see console" }),
3287
+ /* @__PURE__ */ jsx10(
3288
+ "button",
3289
+ {
3290
+ "aria-label": "Close panel",
3291
+ onClick: () => {
3292
+ this.setState({ error: null });
3293
+ this.props.onClose();
3294
+ },
3295
+ style: {
3296
+ background: "none",
3297
+ border: "1px solid #444",
3298
+ borderRadius: "4px",
3299
+ color: "#e6e6e6",
3300
+ cursor: "pointer",
3301
+ padding: "2px 8px"
3302
+ },
3303
+ children: "\xD7"
3304
+ }
3305
+ )
3306
+ ]
3307
+ }
3308
+ );
3309
+ }
3310
+ };
3254
3311
  function App(props) {
3255
3312
  const { recorder, position = "bottom-right", resolveFrame, finalize } = props;
3256
3313
  const [recording, setRecording] = useState4(false);
@@ -3279,12 +3336,23 @@ function App(props) {
3279
3336
  startedAtRef.current = performance.now();
3280
3337
  setElapsedMs(0);
3281
3338
  setResult(null);
3282
- recorder.start();
3339
+ try {
3340
+ recorder.start();
3341
+ } catch (err) {
3342
+ console.warn("[react-perfscope] failed to start recording:", err);
3343
+ return;
3344
+ }
3283
3345
  setRecording(true);
3284
3346
  } else {
3285
- const r = recorder.stop();
3286
- const token = ++resultTokenRef.current;
3287
3347
  setRecording(false);
3348
+ let r;
3349
+ try {
3350
+ r = recorder.stop();
3351
+ } catch (err) {
3352
+ console.warn("[react-perfscope] failed to stop recording:", err);
3353
+ return;
3354
+ }
3355
+ const token = ++resultTokenRef.current;
3288
3356
  setResult(r);
3289
3357
  if (finalize) {
3290
3358
  finalize(r).then((enriched) => {
@@ -3307,14 +3375,31 @@ function App(props) {
3307
3375
  position
3308
3376
  }
3309
3377
  ),
3310
- result !== null && /* @__PURE__ */ jsx10(Panel, { result, position, onClose, resolveFrame })
3378
+ result !== null && /* @__PURE__ */ jsx10(PanelErrorBoundary, { onClose, children: /* @__PURE__ */ jsx10(Panel, { result, position, onClose, resolveFrame }) })
3311
3379
  ] });
3312
3380
  }
3313
3381
 
3314
3382
  // src/mount.tsx
3315
3383
  import { jsx as jsx11 } from "preact/jsx-runtime";
3316
3384
  function mount(opts) {
3317
- const { recorder, position = "bottom-right", host = document.body, resolveFrame, finalize } = opts;
3385
+ if (!opts.host && !document.body) {
3386
+ let torn = false;
3387
+ let unmount = null;
3388
+ const onReady = () => {
3389
+ if (torn || !document.body) return;
3390
+ unmount = mountNow(opts, document.body);
3391
+ };
3392
+ document.addEventListener("DOMContentLoaded", onReady, { once: true });
3393
+ return () => {
3394
+ torn = true;
3395
+ document.removeEventListener("DOMContentLoaded", onReady);
3396
+ unmount?.();
3397
+ };
3398
+ }
3399
+ return mountNow(opts, opts.host ?? document.body);
3400
+ }
3401
+ function mountNow(opts, host) {
3402
+ const { recorder, position = "bottom-right", resolveFrame, finalize } = opts;
3318
3403
  if (host.querySelector("[data-perfscope-host]")) {
3319
3404
  console.warn("[react-perfscope] mount(): a widget is already mounted here \u2014 ignoring this call");
3320
3405
  return () => {