@bfme-technology/spa 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/index.d.ts +10 -0
- package/index.js +1 -0
- package/package.json +21 -0
- package/src/focusAreaLoadElement.js +244 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
export type FocusAreaLoadElementProps = {
|
|
4
|
+
areaName: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
declare const FocusAreaLoadElement: React.FC<FocusAreaLoadElementProps>;
|
|
9
|
+
export default FocusAreaLoadElement;
|
|
10
|
+
export { FocusAreaLoadElement };
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FocusAreaLoadElement } from "./src/focusAreaLoadElement.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bfme-technology/spa",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./index.js",
|
|
9
|
+
"module": "./index.js",
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"import": "./index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": ">=18",
|
|
19
|
+
"react-dom": ">=18"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, { Profiler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const parseThreshold = (value, fallback) => {
|
|
4
|
+
const parsed = Number(value);
|
|
5
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const getPerformanceLevel = (mountDurationMs, thresholdConfig) => {
|
|
9
|
+
if (mountDurationMs === null) return "measuring";
|
|
10
|
+
if (mountDurationMs < thresholdConfig.warningMs) return "good";
|
|
11
|
+
if (mountDurationMs < thresholdConfig.slowMs) return "warning";
|
|
12
|
+
return "slow";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getPerformanceLabel = (performanceLevel) => {
|
|
16
|
+
if (performanceLevel === "good") return "Good";
|
|
17
|
+
if (performanceLevel === "warning") return "Warning";
|
|
18
|
+
if (performanceLevel === "slow") return "Slow";
|
|
19
|
+
return "Measuring";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const tones = {
|
|
23
|
+
good: {
|
|
24
|
+
badge: { border: "1px solid #6ee7b7", background: "#d1fae5", color: "#065f46" },
|
|
25
|
+
tooltip: { border: "1px solid #6ee7b7", background: "#ecfdf5", color: "#064e3b" },
|
|
26
|
+
},
|
|
27
|
+
warning: {
|
|
28
|
+
badge: { border: "1px solid #fcd34d", background: "#fef3c7", color: "#92400e" },
|
|
29
|
+
tooltip: { border: "1px solid #fcd34d", background: "#fffbeb", color: "#78350f" },
|
|
30
|
+
},
|
|
31
|
+
slow: {
|
|
32
|
+
badge: { border: "1px solid #fda4af", background: "#ffe4e6", color: "#9f1239" },
|
|
33
|
+
tooltip: { border: "1px solid #fda4af", background: "#fff1f2", color: "#881337" },
|
|
34
|
+
},
|
|
35
|
+
measuring: {
|
|
36
|
+
badge: { border: "1px solid #a5b4fc", background: "#e0e7ff", color: "#3730a3" },
|
|
37
|
+
tooltip: { border: "1px solid #cbd5e1", background: "#ffffff", color: "#334155" },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const styles = {
|
|
42
|
+
container: { position: "relative" },
|
|
43
|
+
overlay: { position: "absolute", right: "0.5rem", top: "0.5rem", zIndex: 70 },
|
|
44
|
+
badge: {
|
|
45
|
+
height: "1.25rem",
|
|
46
|
+
minWidth: "1.25rem",
|
|
47
|
+
borderRadius: "9999px",
|
|
48
|
+
display: "inline-flex",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
padding: "0 0.375rem",
|
|
52
|
+
fontSize: "10px",
|
|
53
|
+
fontWeight: 600,
|
|
54
|
+
lineHeight: 1,
|
|
55
|
+
cursor: "default",
|
|
56
|
+
},
|
|
57
|
+
tooltip: {
|
|
58
|
+
position: "absolute",
|
|
59
|
+
right: 0,
|
|
60
|
+
top: "1.5rem",
|
|
61
|
+
width: "18rem",
|
|
62
|
+
maxWidth: "18rem",
|
|
63
|
+
borderRadius: "0.375rem",
|
|
64
|
+
padding: "0.25rem 0.5rem",
|
|
65
|
+
fontSize: "11px",
|
|
66
|
+
fontWeight: 500,
|
|
67
|
+
boxShadow: "0 1px 2px rgba(0,0,0,0.08)",
|
|
68
|
+
display: "none",
|
|
69
|
+
whiteSpace: "normal",
|
|
70
|
+
overflowWrap: "anywhere",
|
|
71
|
+
wordBreak: "break-word",
|
|
72
|
+
lineHeight: 1.35,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const FocusAreaLoadElement = ({ areaName, children }) => {
|
|
77
|
+
const [mountDurationMs, setMountDurationMs] = useState(null);
|
|
78
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
79
|
+
const hasCapturedMount = useRef(false);
|
|
80
|
+
const mountStartRef = useRef(
|
|
81
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
82
|
+
? performance.now()
|
|
83
|
+
: Date.now()
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const isFalEnabled = useMemo(() => {
|
|
87
|
+
if (typeof window === "undefined") return false;
|
|
88
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
89
|
+
const hashQuery = window.location.hash.includes("?")
|
|
90
|
+
? window.location.hash.slice(window.location.hash.indexOf("?"))
|
|
91
|
+
: "";
|
|
92
|
+
const hashParams = new URLSearchParams(hashQuery);
|
|
93
|
+
|
|
94
|
+
const getParamValue = (key) => {
|
|
95
|
+
if (searchParams.has(key)) {
|
|
96
|
+
return searchParams.get(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hashParams.has(key)) {
|
|
100
|
+
return hashParams.get(key);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const hasParam = (key) => searchParams.has(key) || hashParams.has(key);
|
|
107
|
+
|
|
108
|
+
const showFalParam = (
|
|
109
|
+
getParamValue("showfal") ??
|
|
110
|
+
getParamValue("showFAL") ??
|
|
111
|
+
getParamValue("showFal") ??
|
|
112
|
+
""
|
|
113
|
+
)
|
|
114
|
+
.trim()
|
|
115
|
+
.toLowerCase();
|
|
116
|
+
const hasFlag =
|
|
117
|
+
hasParam("showfal") ||
|
|
118
|
+
hasParam("showFAL") ||
|
|
119
|
+
hasParam("showFal");
|
|
120
|
+
return hasFlag && ["", "1", "true", "yes", "on"].includes(showFalParam);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!isFalEnabled || hasCapturedMount.current) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const resolveNow = () =>
|
|
129
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
130
|
+
? performance.now()
|
|
131
|
+
: Date.now();
|
|
132
|
+
|
|
133
|
+
const finalizeDuration = () => {
|
|
134
|
+
if (hasCapturedMount.current) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const duration = Math.max(0, resolveNow() - mountStartRef.current);
|
|
139
|
+
hasCapturedMount.current = true;
|
|
140
|
+
setMountDurationMs(Number(duration.toFixed(2)));
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const rafId =
|
|
144
|
+
typeof requestAnimationFrame === "function"
|
|
145
|
+
? requestAnimationFrame(finalizeDuration)
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
const timeoutId = setTimeout(finalizeDuration, 120);
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
if (rafId !== null && typeof cancelAnimationFrame === "function") {
|
|
152
|
+
cancelAnimationFrame(rafId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
};
|
|
157
|
+
}, [isFalEnabled]);
|
|
158
|
+
|
|
159
|
+
const thresholdConfig = useMemo(() => {
|
|
160
|
+
const env = typeof process !== "undefined" ? process.env : undefined;
|
|
161
|
+
const warningMs = parseThreshold(env?.VITE_FAL_WARNING_MS, 16);
|
|
162
|
+
const slowMsRaw = parseThreshold(env?.VITE_FAL_SLOW_MS, 50);
|
|
163
|
+
return { warningMs, slowMs: slowMsRaw > warningMs ? slowMsRaw : warningMs + 1 };
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const onRender = useCallback(
|
|
167
|
+
(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
168
|
+
if (!isFalEnabled) return;
|
|
169
|
+
if (phase === "mount") {
|
|
170
|
+
hasCapturedMount.current = true;
|
|
171
|
+
setMountDurationMs(Number(actualDuration.toFixed(2)));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.info("[BusinessPage][Performance]", {
|
|
175
|
+
focusArea: id,
|
|
176
|
+
phase,
|
|
177
|
+
actualDurationMs: Number(actualDuration.toFixed(2)),
|
|
178
|
+
baseDurationMs: Number(baseDuration.toFixed(2)),
|
|
179
|
+
startTimeMs: Number(startTime.toFixed(2)),
|
|
180
|
+
commitTimeMs: Number(commitTime.toFixed(2)),
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
[isFalEnabled]
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const currentElapsedMs =
|
|
187
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
188
|
+
? performance.now() - mountStartRef.current
|
|
189
|
+
: Date.now() - mountStartRef.current;
|
|
190
|
+
const effectiveDurationMs = mountDurationMs ?? Math.max(0, Number(currentElapsedMs.toFixed(2)));
|
|
191
|
+
|
|
192
|
+
const tooltipLabel =
|
|
193
|
+
`${areaName} load: ${effectiveDurationMs.toFixed(2)} ms`;
|
|
194
|
+
|
|
195
|
+
const performanceLevel = getPerformanceLevel(effectiveDurationMs, thresholdConfig);
|
|
196
|
+
const performanceLabel = getPerformanceLabel(performanceLevel);
|
|
197
|
+
const tooltipWithThresholds = `${tooltipLabel} · ${performanceLabel} (good < ${thresholdConfig.warningMs}ms, slow ≥ ${thresholdConfig.slowMs}ms)`;
|
|
198
|
+
const mountDurationLabel = `${effectiveDurationMs.toFixed(0)}ms`;
|
|
199
|
+
const tone = tones[performanceLevel];
|
|
200
|
+
|
|
201
|
+
return React.createElement(
|
|
202
|
+
"div",
|
|
203
|
+
{ style: styles.container },
|
|
204
|
+
isFalEnabled
|
|
205
|
+
? React.createElement(
|
|
206
|
+
"div",
|
|
207
|
+
{ style: styles.overlay },
|
|
208
|
+
React.createElement(
|
|
209
|
+
"div",
|
|
210
|
+
{
|
|
211
|
+
onMouseEnter: () => setIsHovering(true),
|
|
212
|
+
onMouseLeave: () => setIsHovering(false),
|
|
213
|
+
onFocus: () => setIsHovering(true),
|
|
214
|
+
onBlur: () => setIsHovering(false),
|
|
215
|
+
},
|
|
216
|
+
React.createElement(
|
|
217
|
+
"span",
|
|
218
|
+
{
|
|
219
|
+
"aria-label": tooltipLabel,
|
|
220
|
+
title: tooltipLabel,
|
|
221
|
+
tabIndex: 0,
|
|
222
|
+
style: { ...styles.badge, ...tone.badge },
|
|
223
|
+
},
|
|
224
|
+
mountDurationLabel
|
|
225
|
+
),
|
|
226
|
+
React.createElement(
|
|
227
|
+
"div",
|
|
228
|
+
{
|
|
229
|
+
style: {
|
|
230
|
+
...styles.tooltip,
|
|
231
|
+
...tone.tooltip,
|
|
232
|
+
display: isHovering ? "block" : "none",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
tooltipWithThresholds
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
: null,
|
|
240
|
+
React.createElement(Profiler, { id: areaName, onRender }, children)
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default FocusAreaLoadElement;
|