@piyawat37/performance-overlay 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # React performance overlay
1
+ # Performance overlay
2
2
 
3
- An opt-in browser diagnostic overlay for React 18 and 19. Import the component and styles:
3
+ An opt-in browser diagnostic overlay for any JavaScript application, with a React 18/19 adapter. Import the component and styles in React:
4
4
 
5
5
  ```tsx
6
6
  import { PerformanceOverlay } from "@piyawat37/performance-overlay";
@@ -9,6 +9,21 @@ import "@piyawat37/performance-overlay/style.css";
9
9
  <PerformanceOverlay enabled={import.meta.env.DEV} />
10
10
  ```
11
11
 
12
+ ### JavaScript, Vue, Angular and other frameworks
13
+
14
+ The vanilla entry point does not depend on React. Import the stylesheet once using your bundler, mount after `document.body` is available, and destroy the overlay when your app or component is disposed:
15
+
16
+ ```ts
17
+ import { mountPerformanceOverlay } from "@piyawat37/performance-overlay/vanilla";
18
+ import "@piyawat37/performance-overlay/style.css";
19
+
20
+ const overlay = mountPerformanceOverlay({ enabled: import.meta.env.DEV });
21
+ // Later: overlay.setEnabled(true) or overlay.setEnabled(false);
22
+ // On teardown: overlay.destroy();
23
+ ```
24
+
25
+ Use your own framework's environment flag instead of `import.meta.env.DEV` where applicable. Mount in the browser (for example Vue `onMounted` or Angular `ngAfterViewInit`), and call `destroy()` when unmounting. For custom integrations, `@piyawat37/performance-overlay/core` exports `observePerformance`, `snapshotDom`, `groupChildren`, `assess`, and threshold types. `observePerformance(callback)` returns a stop function. The React adapter keeps the original root import API. React and React DOM are optional peer dependencies; React applications must provide React.
26
+
12
27
  `enabled` defaults to `false`. When disabled or collapsed, sampling stops. `defaultOpen` sets the initial expanded state. Sampling pauses in a hidden tab. Measurements remain in the browser; nothing is sent elsewhere.
13
28
 
14
29
  The overlay shows OK, Watch, Investigate, or Unavailable, with suggested investigation steps for Watch and Investigate. It reports FPS, JavaScript heap, DOM element count, and the longest long task and number of long tasks in each sampling interval. A long task occupies the main thread for at least 50 ms; this count and duration are not CPU usage. The overlay cannot measure system CPU or GPU usage. Heap is a non-standard browser API, not total RAM; long tasks and heap are unavailable in some browsers. Sampling and the overlay itself add overhead.
@@ -37,3 +52,7 @@ Override thresholds with whole `{ warning, critical }` pairs. FPS is lower-is-wo
37
52
  ```
38
53
 
39
54
  To develop the package: `npm install`, `npm run build`, then `node --test test/*.test.mjs`.
55
+
56
+ ### Explore the DOM tree
57
+
58
+ Select **Explore DOM tree** under DOM elements to capture a snapshot and navigate from `document body` through child groups. Siblings with the same tag, role and ID are grouped; counts include those elements and all their descendants. Each group shows its share of its parent group. Select **Explore** to drill down, a breadcrumb to go back, and **Highlight** to outline up to 50 connected and visible elements. Use **Refresh DOM scan** after the page changes. The overlay is excluded from counts. The snapshot counts hidden descendants and does not include shadow roots or iframe documents. A large group is a lead to investigate, not proof that it is slow.
@@ -1,7 +1,4 @@
1
- import type { OverlayThresholds } from "./thresholds.js";
2
- export type PerformanceOverlayProps = {
3
- enabled?: boolean;
4
- defaultOpen?: boolean;
5
- thresholds?: Partial<OverlayThresholds>;
6
- };
7
- export declare function PerformanceOverlay({ enabled, defaultOpen, thresholds }: PerformanceOverlayProps): import("react").JSX.Element | null;
1
+ import type { OverlayOptions } from "./vanilla.js";
2
+ export type PerformanceOverlayProps = OverlayOptions;
3
+ /** React adapter for the framework-independent overlay. */
4
+ export declare function PerformanceOverlay({ enabled, defaultOpen, thresholds }: PerformanceOverlayProps): null;
@@ -1,60 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { assess, defaultThresholds } from "./thresholds.js";
4
- const advice = {
5
- fps: "Check Performance recording for slow frames, rendering and animations. Compare with this device’s refresh rate.",
6
- heap: "Compare heap snapshots after repeating the same action; inspect retained objects and detached DOM nodes.",
7
- nodes: "Inspect large lists, tables and hidden content; consider pagination or virtualization.",
8
- tasks: "Record Performance and inspect the longest main-thread tasks, scripts and React renders.",
9
- };
1
+ import { useEffect, useRef } from "react";
2
+ import { mountPerformanceOverlay } from "./vanilla.js";
3
+ /** React adapter for the framework-independent overlay. */
10
4
  export function PerformanceOverlay({ enabled = false, defaultOpen = false, thresholds }) {
11
- const [open, setOpen] = useState(defaultOpen);
12
- const [sample, setSample] = useState(null);
13
- const [visible, setVisible] = useState(() => typeof document === "undefined" || !document.hidden);
14
- const limits = { ...defaultThresholds, ...thresholds };
5
+ const handle = useRef(null);
15
6
  useEffect(() => {
16
- const update = () => setVisible(!document.hidden);
17
- document.addEventListener("visibilitychange", update);
18
- return () => document.removeEventListener("visibilitychange", update);
19
- }, []);
20
- useEffect(() => {
21
- if (!enabled || !open || !visible) {
22
- setSample(null);
23
- return;
24
- }
25
- let frames = 0;
26
- let last = performance.now();
27
- let longTasks = 0;
28
- let maxTaskMs = 0;
29
- let frameId = 0;
30
- const tick = () => { frames += 1; frameId = requestAnimationFrame(tick); };
31
- frameId = requestAnimationFrame(tick);
32
- const supported = typeof PerformanceObserver !== "undefined" && PerformanceObserver.supportedEntryTypes?.includes("longtask");
33
- const observer = supported ? new PerformanceObserver((list) => {
34
- for (const entry of list.getEntries()) {
35
- longTasks += 1;
36
- maxTaskMs = Math.max(maxTaskMs, entry.duration);
37
- }
38
- }) : undefined;
39
- observer?.observe({ entryTypes: ["longtask"] });
40
- const timer = window.setInterval(() => {
41
- const now = performance.now();
42
- const memory = performance.memory;
43
- setSample({ fps: Math.round(frames * 1000 / (now - last)), heapMb: memory ? memory.usedJSHeapSize / 1048576 : null, nodes: document.getElementsByTagName("*").length, longTasks: supported ? longTasks : null, maxTaskMs: supported ? maxTaskMs : null });
44
- frames = 0;
45
- longTasks = 0;
46
- maxTaskMs = 0;
47
- last = now;
48
- }, 1000);
49
- return () => { cancelAnimationFrame(frameId); clearInterval(timer); observer?.disconnect(); };
50
- }, [enabled, open, visible]);
51
- if (!enabled)
52
- return null;
53
- const rows = [
54
- { key: "fps", label: "Frame rate", value: sample ? `${sample.fps} FPS` : "—", status: assess(sample?.fps ?? null, limits.fps, true) },
55
- { key: "heap", label: "JS heap", value: sample?.heapMb != null ? `${sample.heapMb.toFixed(0)} MB` : "Unavailable", status: assess(sample?.heapMb ?? null, limits.heapMb) },
56
- { key: "nodes", label: "DOM elements", value: sample ? sample.nodes.toLocaleString() : "—", status: assess(sample?.nodes ?? null, limits.nodes) },
57
- { key: "tasks", label: "Longest task / sec", value: sample?.maxTaskMs != null ? `${sample.maxTaskMs.toFixed(0)} ms (${sample.longTasks})` : "Unavailable", status: assess(sample?.maxTaskMs ?? null, limits.longTaskMs) },
58
- ];
59
- return (_jsxs("aside", { className: "dev-performance", "aria-label": "Development performance monitor", children: [_jsx("button", { type: "button", className: "dev-performance__toggle", "aria-expanded": open, onClick: () => setOpen(!open), children: open ? "Hide performance" : "Performance" }), open && _jsxs("div", { className: "dev-performance__body", children: [!visible && _jsx("p", { children: "Paused while this tab is hidden." }), rows.map((row) => _jsxs("div", { className: "dev-performance__metric", children: [_jsxs("div", { className: "dev-performance__line", children: [_jsx("span", { children: row.label }), _jsx("strong", { children: row.value })] }), _jsx("span", { className: `dev-performance__status dev-performance__status--${row.status}`, children: row.status === "ok" ? "OK" : row.status === "warning" ? "Watch" : row.status === "critical" ? "Investigate" : "Unavailable" }), (row.status === "warning" || row.status === "critical") && _jsx("p", { children: advice[row.key] })] }, row.key)), _jsx("small", { children: "Indicative thresholds, not CPU/GPU usage. FPS varies by device; heap and task support vary by browser." })] })] }));
7
+ const overlay = mountPerformanceOverlay({ defaultOpen, thresholds });
8
+ handle.current = overlay;
9
+ return () => { handle.current = null; overlay.destroy(); };
10
+ }, [defaultOpen, thresholds]);
11
+ useEffect(() => { handle.current?.setEnabled(enabled); }, [enabled, defaultOpen, thresholds]);
12
+ return null;
60
13
  }
package/dist/core.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { assess, defaultThresholds } from "./thresholds.js";
2
+ export type { OverlayThresholds, Threshold, Status } from "./thresholds.js";
3
+ export { groupChildren, snapshotDom } from "./domTree.js";
4
+ export type { DomGroup, DomSnapshot } from "./domTree.js";
5
+ export type PerformanceSample = {
6
+ fps: number;
7
+ heapMb: number | null;
8
+ nodes: number;
9
+ longTasks: number | null;
10
+ maxTaskMs: number | null;
11
+ };
12
+ /** Starts sampling in the current document. Call the returned function to stop. */
13
+ export declare function observePerformance(onSample: (sample: PerformanceSample) => void): () => void;
package/dist/core.js ADDED
@@ -0,0 +1,30 @@
1
+ export { assess, defaultThresholds } from "./thresholds.js";
2
+ export { groupChildren, snapshotDom } from "./domTree.js";
3
+ /** Starts sampling in the current document. Call the returned function to stop. */
4
+ export function observePerformance(onSample) {
5
+ let frames = 0;
6
+ let last = performance.now();
7
+ let longTasks = 0;
8
+ let maxTaskMs = 0;
9
+ let frameId = 0;
10
+ const tick = () => { frames++; frameId = requestAnimationFrame(tick); };
11
+ frameId = requestAnimationFrame(tick);
12
+ const supported = typeof PerformanceObserver !== "undefined" && PerformanceObserver.supportedEntryTypes?.includes("longtask");
13
+ const observer = supported ? new PerformanceObserver((list) => {
14
+ for (const entry of list.getEntries()) {
15
+ longTasks++;
16
+ maxTaskMs = Math.max(maxTaskMs, entry.duration);
17
+ }
18
+ }) : undefined;
19
+ observer?.observe({ entryTypes: ["longtask"] });
20
+ const timer = window.setInterval(() => {
21
+ const now = performance.now();
22
+ const memory = performance.memory;
23
+ onSample({ fps: Math.round(frames * 1000 / (now - last)), heapMb: memory ? memory.usedJSHeapSize / 1048576 : null, nodes: document.getElementsByTagName("*").length, longTasks: supported ? longTasks : null, maxTaskMs: supported ? maxTaskMs : null });
24
+ frames = 0;
25
+ longTasks = 0;
26
+ maxTaskMs = 0;
27
+ last = now;
28
+ }, 1000);
29
+ return () => { cancelAnimationFrame(frameId); clearInterval(timer); observer?.disconnect(); };
30
+ }
@@ -0,0 +1,12 @@
1
+ export type DomGroup = {
2
+ label: string;
3
+ elements: Element[];
4
+ count: number;
5
+ parentCount: number;
6
+ };
7
+ export type DomSnapshot = {
8
+ counts: Map<Element, number>;
9
+ roots: DomGroup[];
10
+ };
11
+ export declare function groupChildren(elements: Element[], counts: Map<Element, number>, parentCount: number): DomGroup[];
12
+ export declare function snapshotDom(body: Element): DomSnapshot;
@@ -0,0 +1,36 @@
1
+ function label(element) {
2
+ const id = element.id ? `#${element.id}` : "";
3
+ const role = element.getAttribute("role");
4
+ return `${element.tagName.toLowerCase()}${id}${role ? ` [${role}]` : ""}`;
5
+ }
6
+ export function groupChildren(elements, counts, parentCount) {
7
+ const groups = new Map();
8
+ for (const parent of elements) {
9
+ for (const child of Array.from(parent.children)) {
10
+ const count = counts.get(child);
11
+ if (!count)
12
+ continue;
13
+ // Group siblings by tag and role; IDs remain separate to preserve identifiable targets.
14
+ const name = label(child);
15
+ const group = groups.get(name) ?? { label: name, elements: [], count: 0, parentCount };
16
+ group.elements.push(child);
17
+ group.count += count;
18
+ groups.set(name, group);
19
+ }
20
+ }
21
+ return Array.from(groups.values()).sort((a, b) => b.count - a.count);
22
+ }
23
+ export function snapshotDom(body) {
24
+ const all = Array.from(body.querySelectorAll("*"));
25
+ const counts = new Map();
26
+ for (let index = all.length - 1; index >= 0; index--) {
27
+ const element = all[index];
28
+ if (element.closest("[data-performance-overlay]"))
29
+ continue;
30
+ const count = (counts.get(element) ?? 0) + 1;
31
+ counts.set(element, count);
32
+ if (element.parentElement)
33
+ counts.set(element.parentElement, (counts.get(element.parentElement) ?? 0) + count);
34
+ }
35
+ return { counts, roots: groupChildren([body], counts, counts.get(body) ?? 0) };
36
+ }
package/dist/style.css CHANGED
@@ -54,3 +54,13 @@
54
54
  .dev-performance__status--critical { color: #ffaaaa; }
55
55
  .dev-performance__status--unavailable { color: #c5d1dc; }
56
56
  .dev-performance__body p { margin: 4px 0; color: #f5f7fa; }
57
+ .dev-performance__body .dev-performance__inspector { display: block; padding: 0; }
58
+ .dev-performance__inspector button { display: block; width: 100%; margin-top: 5px; padding: 6px 8px; border: 1px solid #718297; border-radius: 4px; color: #f5f7fa; background: #26394a; font: inherit; text-align: left; overflow-wrap: anywhere; cursor: pointer; }
59
+ .dev-performance__inspector button:hover, .dev-performance__inspector button[aria-pressed="true"] { background: #38546b; }
60
+ .dev-performance__inspector button:focus-visible { outline: 2px solid #8bc6ff; outline-offset: 2px; }
61
+ .dev-performance__highlight { position: fixed; z-index: 2147483646; box-sizing: border-box; border: 3px solid #ffbd45; background: #ffbd4533; pointer-events: none; }
62
+ .dev-performance__body .dev-performance__path { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 4px; padding: 4px 0; }
63
+ .dev-performance__path button { display: inline-block; width: auto; margin: 0; }
64
+ .dev-performance__body .dev-performance__group { display: block; border-top: 1px solid #536170; padding: 8px 0; overflow-wrap: anywhere; }
65
+ .dev-performance__body .dev-performance__actions { display: flex; justify-content: flex-start; gap: 6px; padding: 0; }
66
+ .dev-performance__actions button { width: auto; }
@@ -0,0 +1,12 @@
1
+ import type { OverlayThresholds } from "./core.js";
2
+ export type OverlayOptions = {
3
+ enabled?: boolean;
4
+ defaultOpen?: boolean;
5
+ thresholds?: Partial<OverlayThresholds>;
6
+ };
7
+ export type OverlayHandle = {
8
+ setEnabled(enabled: boolean): void;
9
+ destroy(): void;
10
+ };
11
+ /** Mount an overlay in any browser app. Import the package stylesheet separately. */
12
+ export declare function mountPerformanceOverlay(options?: OverlayOptions): OverlayHandle;
@@ -0,0 +1,166 @@
1
+ import { assess, defaultThresholds, groupChildren, observePerformance, snapshotDom } from "./core.js";
2
+ /** Mount an overlay in any browser app. Import the package stylesheet separately. */
3
+ export function mountPerformanceOverlay(options = {}) {
4
+ let enabled = options.enabled ?? false;
5
+ let open = options.defaultOpen ?? false;
6
+ let destroyed = false;
7
+ let stopSampling;
8
+ let sample = null;
9
+ let snapshot = null;
10
+ let path = [];
11
+ let highlighted = null;
12
+ let boxes = [];
13
+ const limits = { ...defaultThresholds, ...options.thresholds };
14
+ const root = document.createElement("aside");
15
+ root.className = "dev-performance";
16
+ root.setAttribute("data-performance-overlay", "");
17
+ root.setAttribute("aria-label", "Development performance monitor");
18
+ const toggle = document.createElement("button");
19
+ toggle.type = "button";
20
+ toggle.className = "dev-performance__toggle";
21
+ const body = document.createElement("div");
22
+ body.className = "dev-performance__body";
23
+ const metrics = document.createElement("div");
24
+ const inspector = document.createElement("div");
25
+ inspector.className = "dev-performance__inspector";
26
+ const note = document.createElement("small");
27
+ note.textContent = "Indicative thresholds, not CPU/GPU usage. FPS varies by device; heap and task support vary by browser.";
28
+ body.append(metrics, inspector, note);
29
+ root.append(toggle, body);
30
+ const button = (text, action) => {
31
+ const el = document.createElement("button");
32
+ el.type = "button";
33
+ el.textContent = text;
34
+ el.addEventListener("click", action);
35
+ return el;
36
+ };
37
+ const clearHighlights = () => { boxes.forEach((box) => box.remove()); boxes = []; highlighted = null; };
38
+ const updateHighlights = () => {
39
+ boxes.forEach((box) => box.remove());
40
+ boxes = [];
41
+ if (!highlighted)
42
+ return;
43
+ for (const element of highlighted.elements.filter((item) => item.isConnected).slice(0, 50)) {
44
+ const rect = element.getBoundingClientRect();
45
+ if (!rect.width || !rect.height)
46
+ continue;
47
+ const box = document.createElement("div");
48
+ box.className = "dev-performance__highlight";
49
+ box.setAttribute("data-performance-overlay", "");
50
+ box.setAttribute("aria-hidden", "true");
51
+ Object.assign(box.style, { top: `${rect.top}px`, left: `${rect.left}px`, width: `${rect.width}px`, height: `${rect.height}px` });
52
+ document.body.append(box);
53
+ boxes.push(box);
54
+ }
55
+ };
56
+ const renderInspector = () => {
57
+ inspector.replaceChildren(button(snapshot ? "Refresh DOM scan" : "Explore DOM tree", () => { clearHighlights(); path = []; snapshot = snapshotDom(document.body); renderInspector(); }));
58
+ if (!snapshot)
59
+ return;
60
+ const info = document.createElement("p");
61
+ info.textContent = "Choose a group to explore its children. Counts include each element and its descendants. Large groups may not be slow.";
62
+ inspector.append(info);
63
+ const crumbs = document.createElement("div");
64
+ crumbs.className = "dev-performance__path";
65
+ crumbs.setAttribute("aria-label", "DOM path");
66
+ crumbs.append(button("document body", () => { path = []; clearHighlights(); renderInspector(); }));
67
+ path.forEach((group, index) => crumbs.append(button(`› ${group.label}${group.elements.length > 1 ? ` × ${group.elements.length}` : ""}`, () => { path = path.slice(0, index + 1); clearHighlights(); renderInspector(); })));
68
+ inspector.append(crumbs);
69
+ const current = path[path.length - 1];
70
+ const groups = current ? groupChildren(current.elements, snapshot.counts, current.count) : snapshot.roots;
71
+ if (current) {
72
+ const count = document.createElement("p");
73
+ count.textContent = `${current.count.toLocaleString()} elements · ${current.elements.length.toLocaleString()} matching nodes`;
74
+ inspector.append(count);
75
+ }
76
+ if (!groups.length) {
77
+ const empty = document.createElement("p");
78
+ empty.textContent = "No child elements in this snapshot.";
79
+ inspector.append(empty);
80
+ }
81
+ for (const group of groups) {
82
+ const row = document.createElement("div");
83
+ row.className = "dev-performance__group";
84
+ const label = document.createElement("strong");
85
+ label.textContent = `${group.label}${group.elements.length > 1 ? ` × ${group.elements.length}` : ""}`;
86
+ const count = document.createElement("span");
87
+ count.textContent = `${group.count.toLocaleString()} elements · ${group.parentCount ? Math.round(group.count / group.parentCount * 100) : 0}% of parent`;
88
+ const actions = document.createElement("div");
89
+ actions.className = "dev-performance__actions";
90
+ actions.append(button("Highlight", () => { clearHighlights(); highlighted = group; const first = group.elements.find((item) => item.isConnected && item.getBoundingClientRect().width > 0 && item.getBoundingClientRect().height > 0); first?.scrollIntoView({ block: "nearest", inline: "nearest" }); updateHighlights(); renderInspector(); }), button("Explore", () => { clearHighlights(); path.push(group); renderInspector(); }));
91
+ row.append(label, document.createElement("br"), count, actions);
92
+ inspector.append(row);
93
+ }
94
+ if (highlighted) {
95
+ const msg = document.createElement("p");
96
+ msg.textContent = "Highlighting up to 50 connected elements. ";
97
+ msg.append(button("Clear highlight", () => { clearHighlights(); renderInspector(); }));
98
+ inspector.append(msg);
99
+ }
100
+ };
101
+ const renderMetrics = () => {
102
+ const rows = [
103
+ { label: "Frame rate", value: sample ? `${sample.fps} FPS` : "—", status: assess(sample?.fps ?? null, limits.fps, true), advice: "Check Performance recording for slow frames, rendering and animations. Compare with this device’s refresh rate." },
104
+ { label: "JS heap", value: sample?.heapMb != null ? `${sample.heapMb.toFixed(0)} MB` : "Unavailable", status: assess(sample?.heapMb ?? null, limits.heapMb), advice: "Compare heap snapshots after repeating the same action; inspect retained objects and detached DOM nodes." },
105
+ { label: "DOM elements", value: sample ? sample.nodes.toLocaleString() : "—", status: assess(sample?.nodes ?? null, limits.nodes), advice: "Inspect large lists, tables and hidden content; consider pagination or virtualization." },
106
+ { label: "Longest task / sec", value: sample?.maxTaskMs != null ? `${sample.maxTaskMs.toFixed(0)} ms (${sample.longTasks})` : "Unavailable", status: assess(sample?.maxTaskMs ?? null, limits.longTaskMs), advice: "Record Performance and inspect the longest main-thread tasks and scripts." },
107
+ ];
108
+ rows.forEach((row, index) => {
109
+ let item = metrics.children.item(index);
110
+ if (!item) {
111
+ item = document.createElement("div");
112
+ item.className = "dev-performance__metric";
113
+ const line = document.createElement("div");
114
+ line.className = "dev-performance__line";
115
+ line.append(document.createElement("span"), document.createElement("strong"));
116
+ const status = document.createElement("span");
117
+ const advice = document.createElement("p");
118
+ advice.className = "dev-performance__advice";
119
+ item.append(line, status, advice);
120
+ if (index === 2)
121
+ item.append(inspector);
122
+ metrics.append(item);
123
+ }
124
+ const line = item.children[0];
125
+ line.children[0].textContent = row.label;
126
+ line.children[1].textContent = row.value;
127
+ const status = item.children[1];
128
+ status.className = `dev-performance__status dev-performance__status--${row.status}`;
129
+ status.textContent = row.status === "ok" ? "OK" : row.status === "warning" ? "Watch" : row.status === "critical" ? "Investigate" : "Unavailable";
130
+ const advice = item.children[2];
131
+ advice.textContent = row.advice;
132
+ advice.hidden = row.status !== "warning" && row.status !== "critical";
133
+ });
134
+ };
135
+ const sync = () => {
136
+ stopSampling?.();
137
+ stopSampling = undefined;
138
+ if (!enabled || !open || document.hidden) {
139
+ sample = null;
140
+ clearHighlights();
141
+ }
142
+ if (enabled && open && !document.hidden)
143
+ stopSampling = observePerformance((next) => { sample = next; renderMetrics(); });
144
+ root.hidden = !enabled;
145
+ toggle.textContent = open ? "Hide performance" : "Performance";
146
+ toggle.setAttribute("aria-expanded", String(open));
147
+ body.hidden = !open;
148
+ renderMetrics();
149
+ };
150
+ toggle.addEventListener("click", () => { open = !open; sync(); });
151
+ const visibility = () => sync();
152
+ document.addEventListener("visibilitychange", visibility);
153
+ window.addEventListener("scroll", updateHighlights, true);
154
+ window.addEventListener("resize", updateHighlights);
155
+ document.body.append(root);
156
+ renderInspector();
157
+ sync();
158
+ return {
159
+ setEnabled(value) { if (!destroyed && enabled !== value) {
160
+ enabled = value;
161
+ sync();
162
+ } },
163
+ destroy() { if (destroyed)
164
+ return; destroyed = true; stopSampling?.(); clearHighlights(); document.removeEventListener("visibilitychange", visibility); window.removeEventListener("scroll", updateHighlights, true); window.removeEventListener("resize", updateHighlights); root.remove(); },
165
+ };
166
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@piyawat37/performance-overlay",
3
- "version": "0.1.1",
4
- "description": "A configurable client-side performance overlay for React applications",
3
+ "version": "0.1.2",
4
+ "description": "A framework-independent browser performance overlay with a React adapter",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -12,7 +12,15 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js"
14
14
  },
15
- "./style.css": "./dist/style.css"
15
+ "./style.css": "./dist/style.css",
16
+ "./vanilla": {
17
+ "types": "./dist/vanilla.d.ts",
18
+ "import": "./dist/vanilla.js"
19
+ },
20
+ "./core": {
21
+ "types": "./dist/core.d.ts",
22
+ "import": "./dist/core.js"
23
+ }
16
24
  },
17
25
  "sideEffects": [
18
26
  "**/*.css"
@@ -22,10 +30,23 @@
22
30
  "prepare": "npm run build"
23
31
  },
24
32
  "peerDependencies": {
25
- "react": ">=18 <20"
33
+ "react": ">=18 <20",
34
+ "react-dom": ">=18 <20"
26
35
  },
27
36
  "devDependencies": {
28
37
  "@types/react": "^19.0.0",
38
+ "@types/react-dom": "^19.0.0",
39
+ "jsdom": "^31.0.0",
40
+ "react": "^19.3.1",
41
+ "react-dom": "^19.3.1",
29
42
  "typescript": "^5.9.3"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "react": {
46
+ "optional": true
47
+ },
48
+ "react-dom": {
49
+ "optional": true
50
+ }
30
51
  }
31
52
  }