@fluxiapi/react 0.3.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,1318 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ FluxDevTools: () => FluxDevTools,
34
+ FluxProvider: () => FluxProvider,
35
+ ScannerBridge: () => ScannerBridge,
36
+ createFluxSWRMiddleware: () => createFluxSWRMiddleware,
37
+ extractQueryMetrics: () => extractQueryMetrics,
38
+ fluxSWRMiddleware: () => fluxSWRMiddleware,
39
+ getGlobalBridge: () => getGlobalBridge,
40
+ resetGlobalBridge: () => resetGlobalBridge,
41
+ useFlux: () => useFlux,
42
+ useFluxBridge: () => useFluxBridge,
43
+ useFluxReport: () => useFluxReport,
44
+ useFluxRequests: () => useFluxRequests,
45
+ useFluxScanning: () => useFluxScanning,
46
+ useFluxScore: () => useFluxScore,
47
+ useFluxState: () => useFluxState,
48
+ useFluxViolations: () => useFluxViolations,
49
+ wrapQueryClient: () => wrapQueryClient
50
+ });
51
+ module.exports = __toCommonJS(src_exports);
52
+
53
+ // src/devtools/FluxDevTools.tsx
54
+ var import_react3 = require("react");
55
+
56
+ // src/context.tsx
57
+ var import_react = require("react");
58
+
59
+ // src/scanner-bridge.ts
60
+ var ScannerBridge = class {
61
+ constructor(config = {}) {
62
+ this._listeners = /* @__PURE__ */ new Set();
63
+ this._analyzeTimer = null;
64
+ this._tickTimer = null;
65
+ // Lazy-loaded scan engine classes
66
+ this._scanner = null;
67
+ this._analyzer = null;
68
+ this._config = {
69
+ network: config.network ?? "wifi",
70
+ analysisInterval: config.analysisInterval ?? 3e3,
71
+ autoStart: config.autoStart ?? true,
72
+ verbose: config.verbose ?? false
73
+ };
74
+ this._state = {
75
+ scanning: false,
76
+ requests: [],
77
+ report: null,
78
+ score: 100,
79
+ violations: [],
80
+ startTime: 0,
81
+ elapsed: 0,
82
+ network: this._config.network,
83
+ framework: null
84
+ };
85
+ }
86
+ // ─── State Management ───────────────────────────────────────
87
+ get state() {
88
+ return this._state;
89
+ }
90
+ subscribe(listener) {
91
+ this._listeners.add(listener);
92
+ return () => this._listeners.delete(listener);
93
+ }
94
+ _emit() {
95
+ const snapshot = { ...this._state };
96
+ this._listeners.forEach((fn) => fn(snapshot));
97
+ }
98
+ _update(partial) {
99
+ this._state = { ...this._state, ...partial };
100
+ this._emit();
101
+ }
102
+ // ─── Scan Lifecycle ─────────────────────────────────────────
103
+ async start() {
104
+ if (this._state.scanning) return;
105
+ try {
106
+ const scanModule = await import("@fluxiapi/scan");
107
+ this._scanner = new scanModule.FluxScanner({
108
+ duration: Infinity,
109
+ // We control the stop
110
+ network: this._config.network,
111
+ verbose: this._config.verbose
112
+ });
113
+ this._analyzer = new scanModule.FluxAnalyzer({
114
+ network: this._config.network
115
+ });
116
+ this._scanner.start();
117
+ this._update({
118
+ scanning: true,
119
+ startTime: Date.now(),
120
+ elapsed: 0,
121
+ requests: [],
122
+ report: null,
123
+ violations: [],
124
+ score: 100
125
+ });
126
+ this._tickTimer = setInterval(() => {
127
+ if (!this._scanner) return;
128
+ const requests = this._scanner.getRequests?.() ?? [];
129
+ this._update({
130
+ elapsed: Math.round((Date.now() - this._state.startTime) / 1e3),
131
+ requests
132
+ });
133
+ }, 500);
134
+ this._analyzeTimer = setInterval(() => {
135
+ this._runAnalysis();
136
+ }, this._config.analysisInterval);
137
+ if (this._config.verbose) {
138
+ console.log("[FluxAPI] Scanner started");
139
+ }
140
+ } catch (err) {
141
+ console.error("[FluxAPI] Failed to start scanner:", err);
142
+ }
143
+ }
144
+ stop() {
145
+ if (!this._state.scanning || !this._scanner) return null;
146
+ if (this._tickTimer) clearInterval(this._tickTimer);
147
+ if (this._analyzeTimer) clearInterval(this._analyzeTimer);
148
+ this._tickTimer = null;
149
+ this._analyzeTimer = null;
150
+ const session = this._scanner.stop();
151
+ const report = this._analyzer?.analyze(session) ?? null;
152
+ this._update({
153
+ scanning: false,
154
+ report,
155
+ score: report?.score?.overall ?? 100,
156
+ violations: report?.violations ?? [],
157
+ framework: session?.stack?.framework?.name ?? null
158
+ });
159
+ if (this._config.verbose) {
160
+ console.log("[FluxAPI] Scanner stopped. Score:", report?.score?.overall);
161
+ }
162
+ return report;
163
+ }
164
+ reset() {
165
+ this.stop();
166
+ this._update({
167
+ scanning: false,
168
+ requests: [],
169
+ report: null,
170
+ score: 100,
171
+ violations: [],
172
+ startTime: 0,
173
+ elapsed: 0,
174
+ framework: null
175
+ });
176
+ }
177
+ setNetwork(network) {
178
+ this._config.network = network;
179
+ this._update({ network });
180
+ if (this._analyzer) {
181
+ try {
182
+ const scanModule = require("@fluxiapi/scan");
183
+ this._analyzer = new scanModule.FluxAnalyzer({ network });
184
+ } catch {
185
+ }
186
+ }
187
+ }
188
+ // ─── Analysis ───────────────────────────────────────────────
189
+ _runAnalysis() {
190
+ if (!this._scanner || !this._analyzer) return;
191
+ try {
192
+ const session = this._scanner.stop();
193
+ this._scanner.start();
194
+ const report = this._analyzer.analyze(session);
195
+ this._update({
196
+ report,
197
+ score: report?.score?.overall ?? 100,
198
+ violations: report?.violations ?? [],
199
+ framework: session?.stack?.framework?.name ?? null
200
+ });
201
+ } catch (err) {
202
+ if (this._config.verbose) {
203
+ console.error("[FluxAPI] Analysis error:", err);
204
+ }
205
+ }
206
+ }
207
+ // ─── TanStack Query Integration ─────────────────────────────
208
+ /**
209
+ * Capture a TanStack Query event. Called by FluxQueryClient wrapper.
210
+ */
211
+ captureQueryEvent(event) {
212
+ if (this._config.verbose) {
213
+ console.log("[FluxAPI] Query event:", event.type, event.queryKey);
214
+ }
215
+ }
216
+ /**
217
+ * Capture a SWR event.
218
+ */
219
+ captureSWREvent(event) {
220
+ if (this._config.verbose) {
221
+ console.log("[FluxAPI] SWR event:", event.type, event.key);
222
+ }
223
+ }
224
+ destroy() {
225
+ this.stop();
226
+ this._listeners.clear();
227
+ this._scanner = null;
228
+ this._analyzer = null;
229
+ }
230
+ };
231
+ var _globalBridge = null;
232
+ function getGlobalBridge(config) {
233
+ if (!_globalBridge) {
234
+ _globalBridge = new ScannerBridge(config);
235
+ }
236
+ return _globalBridge;
237
+ }
238
+ function resetGlobalBridge() {
239
+ _globalBridge?.destroy();
240
+ _globalBridge = null;
241
+ }
242
+
243
+ // src/context.tsx
244
+ var import_jsx_runtime = require("react/jsx-runtime");
245
+ var FluxContext = (0, import_react.createContext)(null);
246
+ function FluxProvider({
247
+ children,
248
+ network,
249
+ analysisInterval,
250
+ autoStart = true,
251
+ verbose = false,
252
+ bridge: customBridge
253
+ }) {
254
+ const bridgeRef = (0, import_react.useRef)(
255
+ customBridge ?? getGlobalBridge({ network, analysisInterval, autoStart, verbose })
256
+ );
257
+ const bridge = bridgeRef.current;
258
+ const state = (0, import_react.useSyncExternalStore)(
259
+ (callback) => bridge.subscribe(callback),
260
+ () => bridge.state,
261
+ () => bridge.state
262
+ // server snapshot
263
+ );
264
+ (0, import_react.useEffect)(() => {
265
+ if (autoStart && !bridge.state.scanning) {
266
+ bridge.start();
267
+ }
268
+ return () => {
269
+ };
270
+ }, [autoStart, bridge]);
271
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FluxContext.Provider, { value: { bridge, state }, children });
272
+ }
273
+ function useFlux() {
274
+ const ctx = (0, import_react.useContext)(FluxContext);
275
+ if (!ctx) {
276
+ throw new Error(
277
+ "useFlux must be used within a <FluxProvider>. Wrap your app with <FluxProvider> or use <FluxDevTools /> which includes it."
278
+ );
279
+ }
280
+ return ctx;
281
+ }
282
+ function useFluxBridge() {
283
+ return useFlux().bridge;
284
+ }
285
+ function useFluxState() {
286
+ return useFlux().state;
287
+ }
288
+
289
+ // src/devtools/styles.ts
290
+ var COLORS = {
291
+ bg: "#0f0f13",
292
+ bg2: "#16161d",
293
+ bg3: "#1e1e28",
294
+ border: "#2a2a3a",
295
+ fg: "#e2e2e6",
296
+ fg2: "#a0a0b0",
297
+ fg3: "#6a6a7a",
298
+ accent: "#7c6afc",
299
+ accent2: "#a78bfa",
300
+ green: "#22c55e",
301
+ blue: "#3b82f6",
302
+ orange: "#f59e0b",
303
+ red: "#ef4444",
304
+ cyan: "#06b6d4"
305
+ };
306
+ var FONTS = {
307
+ sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
308
+ mono: '"SF Mono", "Fira Code", "JetBrains Mono", Menlo, monospace'
309
+ };
310
+ var containerStyle = {
311
+ position: "fixed",
312
+ zIndex: 2147483647,
313
+ fontFamily: FONTS.sans,
314
+ fontSize: "12px",
315
+ lineHeight: "1.5",
316
+ color: COLORS.fg,
317
+ WebkitFontSmoothing: "antialiased"
318
+ };
319
+ var badgeStyle = (score, position) => {
320
+ const color = score >= 90 ? COLORS.green : score >= 70 ? COLORS.blue : score >= 50 ? COLORS.orange : COLORS.red;
321
+ const pos = parsePosition(position);
322
+ return {
323
+ ...pos,
324
+ width: "48px",
325
+ height: "48px",
326
+ borderRadius: "50%",
327
+ background: COLORS.bg,
328
+ border: `2px solid ${color}`,
329
+ cursor: "pointer",
330
+ display: "flex",
331
+ alignItems: "center",
332
+ justifyContent: "center",
333
+ flexDirection: "column",
334
+ boxShadow: `0 4px 20px rgba(0,0,0,0.5), 0 0 0 1px ${COLORS.border}`,
335
+ transition: "all 0.2s ease",
336
+ userSelect: "none"
337
+ };
338
+ };
339
+ var badgeScoreStyle = (score) => ({
340
+ fontSize: "14px",
341
+ fontWeight: 800,
342
+ lineHeight: "1",
343
+ color: score >= 90 ? COLORS.green : score >= 70 ? COLORS.blue : score >= 50 ? COLORS.orange : COLORS.red
344
+ });
345
+ var badgeLabelStyle = {
346
+ fontSize: "6px",
347
+ fontWeight: 600,
348
+ color: COLORS.fg3,
349
+ letterSpacing: "0.5px",
350
+ textTransform: "uppercase"
351
+ };
352
+ var scanningDotStyle = {
353
+ width: "10px",
354
+ height: "10px",
355
+ borderRadius: "50%",
356
+ background: COLORS.accent,
357
+ animation: "fluxPulse 1.5s ease-in-out infinite"
358
+ };
359
+ var panelStyle = (position) => {
360
+ const isRight = position.includes("right");
361
+ const isBottom = position.includes("bottom");
362
+ return {
363
+ position: "fixed",
364
+ [isBottom ? "bottom" : "top"]: "16px",
365
+ [isRight ? "right" : "left"]: "16px",
366
+ width: "380px",
367
+ maxHeight: "min(580px, calc(100vh - 40px))",
368
+ background: COLORS.bg,
369
+ border: `1px solid ${COLORS.border}`,
370
+ borderRadius: "12px",
371
+ boxShadow: "0 8px 40px rgba(0,0,0,0.6)",
372
+ display: "flex",
373
+ flexDirection: "column",
374
+ overflow: "hidden"
375
+ };
376
+ };
377
+ var panelHeaderStyle = {
378
+ display: "flex",
379
+ alignItems: "center",
380
+ justifyContent: "space-between",
381
+ padding: "10px 14px",
382
+ background: COLORS.bg2,
383
+ borderBottom: `1px solid ${COLORS.border}`,
384
+ cursor: "move"
385
+ };
386
+ var panelTitleStyle = {
387
+ display: "flex",
388
+ alignItems: "center",
389
+ gap: "8px",
390
+ fontSize: "13px",
391
+ fontWeight: 700,
392
+ color: COLORS.fg
393
+ };
394
+ var panelCloseBtnStyle = {
395
+ background: "none",
396
+ border: "none",
397
+ color: COLORS.fg3,
398
+ cursor: "pointer",
399
+ fontSize: "16px",
400
+ padding: "2px 4px",
401
+ borderRadius: "4px",
402
+ lineHeight: "1"
403
+ };
404
+ var tabBarStyle = {
405
+ display: "flex",
406
+ borderBottom: `1px solid ${COLORS.border}`,
407
+ background: COLORS.bg2
408
+ };
409
+ var tabStyle = (active) => ({
410
+ flex: 1,
411
+ padding: "7px 0",
412
+ fontSize: "10px",
413
+ fontWeight: 600,
414
+ textAlign: "center",
415
+ cursor: "pointer",
416
+ color: active ? COLORS.accent : COLORS.fg3,
417
+ borderBottom: active ? `2px solid ${COLORS.accent}` : "2px solid transparent",
418
+ background: "none",
419
+ border: "none",
420
+ borderBottomStyle: "solid",
421
+ transition: "all 0.15s",
422
+ textTransform: "uppercase",
423
+ letterSpacing: "0.5px"
424
+ });
425
+ var panelBodyStyle = {
426
+ flex: 1,
427
+ overflow: "auto",
428
+ padding: "12px"
429
+ };
430
+ var scoreRingContainerStyle = {
431
+ display: "flex",
432
+ alignItems: "center",
433
+ gap: "16px",
434
+ marginBottom: "14px"
435
+ };
436
+ var scoreRingStyle = {
437
+ width: "72px",
438
+ height: "72px",
439
+ position: "relative",
440
+ flexShrink: 0
441
+ };
442
+ var scoreNumberStyle = (color) => ({
443
+ position: "absolute",
444
+ top: "50%",
445
+ left: "50%",
446
+ transform: "translate(-50%, -55%)",
447
+ fontSize: "22px",
448
+ fontWeight: 800,
449
+ color,
450
+ lineHeight: "1"
451
+ });
452
+ var scoreGradeStyle = (color) => ({
453
+ position: "absolute",
454
+ top: "50%",
455
+ left: "50%",
456
+ transform: "translate(-50%, 50%)",
457
+ fontSize: "8px",
458
+ fontWeight: 600,
459
+ color,
460
+ textTransform: "uppercase",
461
+ letterSpacing: "0.5px"
462
+ });
463
+ var catRowStyle = {
464
+ display: "flex",
465
+ alignItems: "center",
466
+ gap: "8px",
467
+ marginBottom: "6px",
468
+ fontSize: "11px"
469
+ };
470
+ var catNameStyle = {
471
+ width: "70px",
472
+ color: COLORS.fg2
473
+ };
474
+ var catTrackStyle = {
475
+ flex: 1,
476
+ height: "6px",
477
+ borderRadius: "3px",
478
+ background: COLORS.bg3,
479
+ overflow: "hidden"
480
+ };
481
+ var catFillStyle = (pct, color) => ({
482
+ width: `${pct}%`,
483
+ height: "100%",
484
+ borderRadius: "3px",
485
+ background: color,
486
+ transition: "width 0.5s ease"
487
+ });
488
+ var catPctStyle = (color) => ({
489
+ width: "32px",
490
+ textAlign: "right",
491
+ fontWeight: 700,
492
+ fontFamily: FONTS.mono,
493
+ fontSize: "10px",
494
+ color
495
+ });
496
+ var violationCardStyle = {
497
+ background: COLORS.bg2,
498
+ border: `1px solid ${COLORS.border}`,
499
+ borderRadius: "8px",
500
+ padding: "10px",
501
+ marginBottom: "8px"
502
+ };
503
+ var violationHeaderStyle = {
504
+ display: "flex",
505
+ alignItems: "center",
506
+ gap: "6px",
507
+ marginBottom: "4px"
508
+ };
509
+ var severityDotStyle = (severity) => ({
510
+ width: "8px",
511
+ height: "8px",
512
+ borderRadius: "50%",
513
+ flexShrink: 0,
514
+ background: severity === "critical" ? COLORS.red : severity === "warning" ? COLORS.orange : COLORS.blue
515
+ });
516
+ var ruleIdBadgeStyle = {
517
+ fontSize: "9px",
518
+ fontWeight: 700,
519
+ fontFamily: FONTS.mono,
520
+ color: COLORS.accent,
521
+ padding: "1px 4px",
522
+ borderRadius: "3px",
523
+ background: "rgba(124, 106, 252, 0.12)"
524
+ };
525
+ var violationTitleStyle = {
526
+ fontSize: "11px",
527
+ fontWeight: 600,
528
+ color: COLORS.fg,
529
+ flex: 1
530
+ };
531
+ var violationDescStyle = {
532
+ fontSize: "10px",
533
+ color: COLORS.fg3,
534
+ lineHeight: "1.4"
535
+ };
536
+ var violationImpactStyle = {
537
+ display: "flex",
538
+ gap: "10px",
539
+ marginTop: "6px",
540
+ fontSize: "10px"
541
+ };
542
+ var impactItemStyle = (color) => ({
543
+ color,
544
+ fontWeight: 600,
545
+ fontFamily: FONTS.mono
546
+ });
547
+ var requestRowStyle = {
548
+ display: "flex",
549
+ alignItems: "center",
550
+ gap: "6px",
551
+ padding: "4px 0",
552
+ borderBottom: `1px solid ${COLORS.border}`,
553
+ fontSize: "10px"
554
+ };
555
+ var methodBadgeStyle = (method) => ({
556
+ fontSize: "9px",
557
+ fontWeight: 700,
558
+ fontFamily: FONTS.mono,
559
+ padding: "1px 4px",
560
+ borderRadius: "3px",
561
+ color: method === "GET" ? COLORS.green : method === "POST" ? COLORS.blue : COLORS.orange,
562
+ background: method === "GET" ? "rgba(34, 197, 94, 0.1)" : method === "POST" ? "rgba(59, 130, 246, 0.1)" : "rgba(245, 158, 11, 0.1)",
563
+ minWidth: "32px",
564
+ textAlign: "center"
565
+ });
566
+ var pathStyle = {
567
+ flex: 1,
568
+ color: COLORS.fg2,
569
+ fontFamily: FONTS.mono,
570
+ fontSize: "10px",
571
+ overflow: "hidden",
572
+ textOverflow: "ellipsis",
573
+ whiteSpace: "nowrap"
574
+ };
575
+ var durationStyle = (ms) => ({
576
+ fontFamily: FONTS.mono,
577
+ fontWeight: 600,
578
+ fontSize: "10px",
579
+ color: ms > 500 ? COLORS.red : ms > 200 ? COLORS.orange : COLORS.green,
580
+ minWidth: "40px",
581
+ textAlign: "right"
582
+ });
583
+ var statusStyle = (status) => ({
584
+ fontFamily: FONTS.mono,
585
+ fontSize: "9px",
586
+ fontWeight: 600,
587
+ color: status >= 500 ? COLORS.red : status >= 400 ? COLORS.orange : status >= 300 ? COLORS.blue : COLORS.green,
588
+ minWidth: "24px",
589
+ textAlign: "center"
590
+ });
591
+ var statsGridStyle = {
592
+ display: "grid",
593
+ gridTemplateColumns: "1fr 1fr 1fr",
594
+ gap: "8px",
595
+ marginBottom: "14px"
596
+ };
597
+ var statCardStyle = (color) => ({
598
+ background: COLORS.bg2,
599
+ border: `1px solid ${COLORS.border}`,
600
+ borderRadius: "8px",
601
+ padding: "8px",
602
+ textAlign: "center"
603
+ });
604
+ var statNumStyle = (color) => ({
605
+ fontSize: "18px",
606
+ fontWeight: 800,
607
+ color,
608
+ lineHeight: "1.2"
609
+ });
610
+ var statLabelStyle = {
611
+ fontSize: "9px",
612
+ color: COLORS.fg3,
613
+ textTransform: "uppercase",
614
+ letterSpacing: "0.3px"
615
+ };
616
+ var infoRowStyle = {
617
+ display: "flex",
618
+ alignItems: "center",
619
+ gap: "6px",
620
+ padding: "6px 10px",
621
+ background: COLORS.bg2,
622
+ borderRadius: "6px",
623
+ marginBottom: "8px",
624
+ fontSize: "10px",
625
+ color: COLORS.fg2
626
+ };
627
+ var emptyStateStyle = {
628
+ textAlign: "center",
629
+ padding: "24px",
630
+ color: COLORS.fg3,
631
+ fontSize: "12px"
632
+ };
633
+ function parsePosition(pos) {
634
+ switch (pos) {
635
+ case "bottom-right":
636
+ return { bottom: "16px", right: "16px" };
637
+ case "bottom-left":
638
+ return { bottom: "16px", left: "16px" };
639
+ case "top-right":
640
+ return { top: "16px", right: "16px" };
641
+ case "top-left":
642
+ return { top: "16px", left: "16px" };
643
+ default:
644
+ return { bottom: "16px", right: "16px" };
645
+ }
646
+ }
647
+ var _stylesInjected = false;
648
+ function injectKeyframes() {
649
+ if (_stylesInjected || typeof document === "undefined") return;
650
+ _stylesInjected = true;
651
+ const style = document.createElement("style");
652
+ style.textContent = `
653
+ @keyframes fluxPulse {
654
+ 0%, 100% { opacity: 1; transform: scale(1); }
655
+ 50% { opacity: 0.5; transform: scale(0.85); }
656
+ }
657
+ @keyframes fluxFadeIn {
658
+ from { opacity: 0; transform: translateY(8px); }
659
+ to { opacity: 1; transform: translateY(0); }
660
+ }
661
+ .flux-devtools * { box-sizing: border-box; }
662
+ .flux-devtools ::-webkit-scrollbar { width: 4px; }
663
+ .flux-devtools ::-webkit-scrollbar-track { background: transparent; }
664
+ .flux-devtools ::-webkit-scrollbar-thumb { background: #2a2a3a; border-radius: 2px; }
665
+ `;
666
+ document.head.appendChild(style);
667
+ }
668
+
669
+ // src/devtools/Badge.tsx
670
+ var import_jsx_runtime2 = require("react/jsx-runtime");
671
+ function Badge({ state, position, onClick }) {
672
+ const { score, scanning, violations } = state;
673
+ const critCount = violations.filter((v) => v.severity === "critical").length;
674
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
675
+ "div",
676
+ {
677
+ style: badgeStyle(score, position),
678
+ onClick,
679
+ onMouseEnter: (e) => {
680
+ e.currentTarget.style.transform = "scale(1.1)";
681
+ },
682
+ onMouseLeave: (e) => {
683
+ e.currentTarget.style.transform = "scale(1)";
684
+ },
685
+ title: scanning ? `Scanning... ${state.requests.length} requests` : `API Score: ${score}/100 \xB7 ${violations.length} issues`,
686
+ children: scanning ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
687
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: scanningDotStyle }),
688
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...badgeLabelStyle, marginTop: "2px" }, children: "SCAN" })
689
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
690
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: badgeScoreStyle(score), children: score }),
691
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: badgeLabelStyle, children: critCount > 0 ? `${critCount}!` : "API" })
692
+ ] })
693
+ }
694
+ );
695
+ }
696
+
697
+ // src/devtools/Panel.tsx
698
+ var import_react2 = require("react");
699
+ var import_jsx_runtime3 = require("react/jsx-runtime");
700
+ var RULE_NAMES = {
701
+ E1: "Request Waterfall",
702
+ E2: "Duplicate Requests",
703
+ E3: "N+1 Pattern",
704
+ E4: "Over-fetching",
705
+ E5: "Batchable Requests",
706
+ C1: "No Cache",
707
+ C2: "Under-Caching",
708
+ C3: "Over-Caching",
709
+ C4: "Missing Revalidation",
710
+ P1: "Missing Prefetch",
711
+ P2: "Unnecessary Polling",
712
+ P3: "No Error Recovery",
713
+ P4: "Uncompressed"
714
+ };
715
+ function Panel({ state, bridge, position, onClose }) {
716
+ const [tab, setTab] = (0, import_react2.useState)("overview");
717
+ const { report, violations, requests, scanning, elapsed, score, framework } = state;
718
+ const apiRequests = (0, import_react2.useMemo)(
719
+ () => requests.filter((r) => r.type === "api-rest" || r.type === "api-graphql"),
720
+ [requests]
721
+ );
722
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: panelStyle(position), className: "flux-devtools", children: [
723
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: panelHeaderStyle, children: [
724
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: panelTitleStyle, children: [
725
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u26A1" }),
726
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "FluxAPI" }),
727
+ scanning && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: COLORS.accent, fontSize: "10px", fontWeight: 500 }, children: "\u25CF Recording" }),
728
+ !scanning && report && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: {
729
+ color: score >= 70 ? COLORS.green : score >= 50 ? COLORS.orange : COLORS.red,
730
+ fontSize: "10px",
731
+ fontWeight: 700
732
+ }, children: [
733
+ score,
734
+ "/100"
735
+ ] })
736
+ ] }),
737
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "4px" }, children: [
738
+ scanning ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
739
+ "button",
740
+ {
741
+ style: { ...panelCloseBtnStyle, color: COLORS.red, fontSize: "10px" },
742
+ onClick: () => bridge.stop(),
743
+ children: "\u23F9 Stop"
744
+ }
745
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
746
+ "button",
747
+ {
748
+ style: { ...panelCloseBtnStyle, color: COLORS.green, fontSize: "10px" },
749
+ onClick: () => bridge.start(),
750
+ children: "\u25B6 Scan"
751
+ }
752
+ ),
753
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { style: panelCloseBtnStyle, onClick: onClose, children: "\u2715" })
754
+ ] })
755
+ ] }),
756
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: tabBarStyle, children: ["overview", "violations", "requests"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
757
+ "button",
758
+ {
759
+ style: tabStyle(tab === t),
760
+ onClick: () => setTab(t),
761
+ children: [
762
+ t,
763
+ t === "violations" && violations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: {
764
+ marginLeft: "4px",
765
+ background: COLORS.red,
766
+ color: "#fff",
767
+ borderRadius: "8px",
768
+ padding: "0 5px",
769
+ fontSize: "9px",
770
+ fontWeight: 700
771
+ }, children: violations.length }),
772
+ t === "requests" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: {
773
+ marginLeft: "4px",
774
+ color: COLORS.fg3,
775
+ fontSize: "9px"
776
+ }, children: apiRequests.length })
777
+ ]
778
+ },
779
+ t
780
+ )) }),
781
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: panelBodyStyle, children: [
782
+ tab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
783
+ OverviewTab,
784
+ {
785
+ state,
786
+ apiRequests,
787
+ violations
788
+ }
789
+ ),
790
+ tab === "violations" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ViolationsTab, { violations }),
791
+ tab === "requests" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequestsTab, { requests: apiRequests })
792
+ ] })
793
+ ] });
794
+ }
795
+ function OverviewTab({
796
+ state,
797
+ apiRequests,
798
+ violations
799
+ }) {
800
+ const { report, score, scanning, elapsed, framework } = state;
801
+ const scoreObj = report?.score;
802
+ const color = score >= 90 ? COLORS.green : score >= 70 ? COLORS.blue : score >= 50 ? COLORS.orange : COLORS.red;
803
+ const dash = Math.round(score / 100 * 251);
804
+ const crits = violations.filter((v) => v.severity === "critical").length;
805
+ const warns = violations.filter((v) => v.severity === "warning").length;
806
+ const infos = violations.filter((v) => v.severity === "info").length;
807
+ const totalTimeSaved = violations.reduce((s, v) => s + (v.impact?.timeSavedMs ?? 0), 0);
808
+ const totalReqsSaved = violations.reduce((s, v) => s + (v.impact?.requestsEliminated ?? 0), 0);
809
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
810
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: scoreRingContainerStyle, children: [
811
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: scoreRingStyle, children: [
812
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { viewBox: "0 0 100 100", style: { transform: "rotate(-90deg)" }, children: [
813
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: COLORS.bg3, strokeWidth: "6" }),
814
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
815
+ "circle",
816
+ {
817
+ cx: "50",
818
+ cy: "50",
819
+ r: "40",
820
+ fill: "none",
821
+ stroke: color,
822
+ strokeWidth: "6",
823
+ strokeDasharray: `${dash} 251`,
824
+ strokeLinecap: "round"
825
+ }
826
+ )
827
+ ] }),
828
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: scoreNumberStyle(color), children: score }),
829
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: scoreGradeStyle(color), children: score >= 90 ? "A+" : score >= 70 ? "B" : score >= 50 ? "C" : "F" })
830
+ ] }),
831
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "13px", fontWeight: 700, marginBottom: "4px" }, children: "API Health" }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "10px", color: COLORS.fg3 }, children: [
834
+ apiRequests.length,
835
+ " API calls \xB7 ",
836
+ elapsed,
837
+ "s"
838
+ ] }),
839
+ framework && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "10px", color: COLORS.accent, marginTop: "2px" }, children: [
840
+ "\u269B\uFE0F ",
841
+ framework
842
+ ] })
843
+ ] })
844
+ ] }),
845
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: statsGridStyle, children: [
846
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: statCardStyle(COLORS.red), children: [
847
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: statNumStyle(COLORS.red), children: crits }),
848
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: statLabelStyle, children: "Critical" })
849
+ ] }),
850
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: statCardStyle(COLORS.orange), children: [
851
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: statNumStyle(COLORS.orange), children: warns }),
852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: statLabelStyle, children: "Warnings" })
853
+ ] }),
854
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: statCardStyle(COLORS.blue), children: [
855
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: statNumStyle(COLORS.blue), children: apiRequests.length }),
856
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: statLabelStyle, children: "API Calls" })
857
+ ] })
858
+ ] }),
859
+ (totalTimeSaved > 0 || totalReqsSaved > 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: infoRowStyle, children: [
860
+ totalTimeSaved > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: COLORS.blue, fontWeight: 600 }, children: [
861
+ "\u26A1 ",
862
+ fmtMs(totalTimeSaved),
863
+ " saveable"
864
+ ] }),
865
+ totalReqsSaved > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: COLORS.green, fontWeight: 600 }, children: [
866
+ "\u{1F4C9} ",
867
+ totalReqsSaved,
868
+ " fewer requests"
869
+ ] })
870
+ ] }),
871
+ scoreObj && (() => {
872
+ const cats = scoreObj.categories ?? [];
873
+ const getCat = (cat) => cats.find((c) => c.category === cat)?.score ?? 100;
874
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "12px" }, children: [
875
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CategoryBar, { icon: "\u26A1", name: "Efficiency", score: getCat("efficiency") }),
876
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CategoryBar, { icon: "\u{1F4BE}", name: "Caching", score: getCat("caching") }),
877
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CategoryBar, { icon: "\u{1F504}", name: "Patterns", score: getCat("patterns") })
878
+ ] });
879
+ })(),
880
+ violations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
881
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "10px", fontWeight: 700, color: COLORS.fg2, marginBottom: "6px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: "Top Issues" }),
882
+ violations.slice(0, 3).map((v, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px", padding: "3px 0", fontSize: "10px" }, children: [
883
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: severityDotStyle(v.severity) }),
884
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: ruleIdBadgeStyle, children: v.ruleId }),
885
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: COLORS.fg2, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: v.title }),
886
+ v.impact?.timeSavedMs > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: COLORS.blue, fontSize: "9px", fontWeight: 600 }, children: [
887
+ "\u26A1",
888
+ fmtMs(v.impact.timeSavedMs)
889
+ ] })
890
+ ] }, i))
891
+ ] }),
892
+ violations.length === 0 && !scanning && apiRequests.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: emptyStateStyle, children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u2728" }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: COLORS.green, fontWeight: 700 }, children: "No API issues found!" })
895
+ ] }),
896
+ scanning && apiRequests.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: emptyStateStyle, children: [
897
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u{1F4E1}" }),
898
+ "Waiting for API requests..."
899
+ ] })
900
+ ] });
901
+ }
902
+ function ViolationsTab({ violations }) {
903
+ const [expanded, setExpanded] = (0, import_react2.useState)(null);
904
+ if (violations.length === 0) {
905
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: emptyStateStyle, children: [
906
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u2728" }),
907
+ "No violations detected"
908
+ ] });
909
+ }
910
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: violations.map((v, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: violationCardStyle, children: [
911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
912
+ "div",
913
+ {
914
+ style: { ...violationHeaderStyle, cursor: "pointer" },
915
+ onClick: () => setExpanded(expanded === i ? null : i),
916
+ children: [
917
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: severityDotStyle(v.severity) }),
918
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: ruleIdBadgeStyle, children: v.ruleId }),
919
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { ...violationTitleStyle, fontSize: "10px", color: COLORS.fg3 }, children: RULE_NAMES[v.ruleId] || v.ruleId }),
920
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: COLORS.fg3, fontSize: "10px" }, children: expanded === i ? "\u25BE" : "\u25B8" })
921
+ ]
922
+ }
923
+ ),
924
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...violationTitleStyle, marginTop: "2px" }, children: v.title }),
925
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: violationImpactStyle, children: [
926
+ v.impact?.timeSavedMs > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: impactItemStyle(COLORS.blue), children: [
927
+ "\u26A1 ",
928
+ fmtMs(v.impact.timeSavedMs)
929
+ ] }),
930
+ v.impact?.requestsEliminated > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: impactItemStyle(COLORS.green), children: [
931
+ "\u{1F4C9} ",
932
+ v.impact.requestsEliminated,
933
+ " reqs"
934
+ ] }),
935
+ v.impact?.bandwidthSavedBytes > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: impactItemStyle(COLORS.orange), children: [
936
+ "\u{1F4BE} ",
937
+ fmtBytes(v.impact.bandwidthSavedBytes)
938
+ ] })
939
+ ] }),
940
+ expanded === i && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: "8px" }, children: [
941
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: violationDescStyle, children: v.description }),
942
+ v.affectedEndpoints && v.affectedEndpoints.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: "6px" }, children: [
943
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "9px", color: COLORS.fg3, fontWeight: 600, marginBottom: "3px" }, children: "ENDPOINTS" }),
944
+ v.affectedEndpoints.slice(0, 5).map((ep, j) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "10px", fontFamily: FONTS.mono, color: COLORS.fg2, padding: "1px 0" }, children: ep }, j))
945
+ ] }),
946
+ v.metadata?.fix && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: "6px" }, children: [
947
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "9px", color: COLORS.fg3, fontWeight: 600, marginBottom: "3px" }, children: "FIX" }),
948
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("pre", { style: {
949
+ fontSize: "9px",
950
+ fontFamily: FONTS.mono,
951
+ color: COLORS.accent2,
952
+ background: COLORS.bg,
953
+ borderRadius: "4px",
954
+ padding: "6px",
955
+ overflow: "auto",
956
+ maxHeight: "120px",
957
+ lineHeight: "1.4",
958
+ whiteSpace: "pre-wrap",
959
+ wordBreak: "break-all"
960
+ }, children: typeof v.metadata.fix === "string" ? v.metadata.fix : v.metadata.fix?.code ?? "" })
961
+ ] })
962
+ ] })
963
+ ] }, i)) });
964
+ }
965
+ function RequestsTab({ requests }) {
966
+ const sorted = (0, import_react2.useMemo)(
967
+ () => [...requests].sort((a, b) => b.startTime - a.startTime),
968
+ [requests]
969
+ );
970
+ if (sorted.length === 0) {
971
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: emptyStateStyle, children: [
972
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: "24px", marginBottom: "6px" }, children: "\u{1F4E1}" }),
973
+ "No API requests captured yet"
974
+ ] });
975
+ }
976
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
977
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "10px", color: COLORS.fg3, marginBottom: "6px" }, children: [
978
+ sorted.length,
979
+ " API requests (newest first)"
980
+ ] }),
981
+ sorted.slice(0, 50).map((r, i) => {
982
+ const path = r.urlParts?.pathname ?? new URL(r.url, "http://x").pathname;
983
+ const short = path.length > 40 ? "\u2026" + path.slice(-37) : path;
984
+ const status = r.response?.status ?? 0;
985
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: requestRowStyle, children: [
986
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: methodBadgeStyle(r.method), children: r.method }),
987
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: pathStyle, title: r.url, children: short }),
988
+ status > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: statusStyle(status), children: status }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: durationStyle(r.duration ?? 0), children: [
990
+ r.duration ?? 0,
991
+ "ms"
992
+ ] })
993
+ ] }, i);
994
+ })
995
+ ] });
996
+ }
997
+ function CategoryBar({ icon, name, score }) {
998
+ const color = score >= 70 ? COLORS.green : score >= 50 ? COLORS.orange : COLORS.red;
999
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: catRowStyle, children: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: icon }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: catNameStyle, children: name }),
1002
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: catTrackStyle, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: catFillStyle(score, color) }) }),
1003
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: catPctStyle(color), children: [
1004
+ score,
1005
+ "%"
1006
+ ] })
1007
+ ] });
1008
+ }
1009
+ function fmtMs(ms) {
1010
+ return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
1011
+ }
1012
+ function fmtBytes(bytes) {
1013
+ if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)}MB`;
1014
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
1015
+ return `${bytes}B`;
1016
+ }
1017
+
1018
+ // src/devtools/FluxDevTools.tsx
1019
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1020
+ function FluxDevTools({
1021
+ position = "bottom-right",
1022
+ network,
1023
+ analysisInterval,
1024
+ autoStart = true,
1025
+ defaultOpen = false,
1026
+ verbose = false,
1027
+ bridge,
1028
+ forceShow = false,
1029
+ shortcut = "ctrl+shift+f"
1030
+ }) {
1031
+ if (!forceShow && typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
1032
+ return null;
1033
+ }
1034
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1035
+ FluxProvider,
1036
+ {
1037
+ network,
1038
+ analysisInterval,
1039
+ autoStart,
1040
+ verbose,
1041
+ bridge,
1042
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1043
+ DevToolsInner,
1044
+ {
1045
+ position,
1046
+ defaultOpen,
1047
+ shortcut
1048
+ }
1049
+ )
1050
+ }
1051
+ );
1052
+ }
1053
+ function DevToolsInner({
1054
+ position,
1055
+ defaultOpen,
1056
+ shortcut
1057
+ }) {
1058
+ const [open, setOpen] = (0, import_react3.useState)(defaultOpen);
1059
+ const { bridge, state } = useFlux();
1060
+ (0, import_react3.useEffect)(() => {
1061
+ injectKeyframes();
1062
+ }, []);
1063
+ (0, import_react3.useEffect)(() => {
1064
+ if (!shortcut) return;
1065
+ const parts = shortcut.toLowerCase().split("+");
1066
+ const key = parts.pop();
1067
+ const needCtrl = parts.includes("ctrl") || parts.includes("control");
1068
+ const needShift = parts.includes("shift");
1069
+ const needAlt = parts.includes("alt");
1070
+ const needMeta = parts.includes("meta") || parts.includes("cmd");
1071
+ function handleKeydown(e) {
1072
+ if (e.key.toLowerCase() === key && e.ctrlKey === needCtrl && e.shiftKey === needShift && e.altKey === needAlt && e.metaKey === needMeta) {
1073
+ e.preventDefault();
1074
+ setOpen((prev) => !prev);
1075
+ }
1076
+ }
1077
+ window.addEventListener("keydown", handleKeydown);
1078
+ return () => window.removeEventListener("keydown", handleKeydown);
1079
+ }, [shortcut]);
1080
+ const toggle = (0, import_react3.useCallback)(() => setOpen((prev) => !prev), []);
1081
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: containerStyle, className: "flux-devtools", children: open ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1082
+ Panel,
1083
+ {
1084
+ state,
1085
+ bridge,
1086
+ position,
1087
+ onClose: toggle
1088
+ }
1089
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1090
+ Badge,
1091
+ {
1092
+ state,
1093
+ position,
1094
+ onClick: toggle
1095
+ }
1096
+ ) });
1097
+ }
1098
+
1099
+ // src/hooks/index.ts
1100
+ var import_react4 = require("react");
1101
+ function useFluxScore() {
1102
+ const { state } = useFlux();
1103
+ return (0, import_react4.useMemo)(() => {
1104
+ const score = state.report?.score;
1105
+ const overall = score?.overall ?? 100;
1106
+ const cats = score?.categories ?? [];
1107
+ const getCat = (label) => cats.find((c) => c.category === label)?.score ?? 100;
1108
+ return {
1109
+ overall,
1110
+ grade: score?.grade ?? "excellent",
1111
+ efficiency: getCat("efficiency"),
1112
+ caching: getCat("caching"),
1113
+ patterns: getCat("patterns"),
1114
+ color: overall >= 90 ? "#22c55e" : overall >= 70 ? "#3b82f6" : overall >= 50 ? "#f59e0b" : "#ef4444"
1115
+ };
1116
+ }, [state.report?.score]);
1117
+ }
1118
+ function useFluxViolations(filter) {
1119
+ const { state } = useFlux();
1120
+ return (0, import_react4.useMemo)(() => {
1121
+ let violations = state.violations;
1122
+ if (filter?.severity) {
1123
+ violations = violations.filter((v) => v.severity === filter.severity);
1124
+ }
1125
+ if (filter?.ruleId) {
1126
+ violations = violations.filter((v) => v.ruleId === filter.ruleId);
1127
+ }
1128
+ if (filter?.category) {
1129
+ const prefixMap = {
1130
+ efficiency: ["E1", "E2", "E3", "E4", "E5"],
1131
+ caching: ["C1", "C2", "C3", "C4"],
1132
+ patterns: ["P1", "P2", "P3", "P4"]
1133
+ };
1134
+ const allowed = prefixMap[filter.category] ?? [];
1135
+ violations = violations.filter((v) => allowed.includes(v.ruleId));
1136
+ }
1137
+ return violations;
1138
+ }, [state.violations, filter?.severity, filter?.ruleId, filter?.category]);
1139
+ }
1140
+ function useFluxRequests(filter) {
1141
+ const { state } = useFlux();
1142
+ return (0, import_react4.useMemo)(() => {
1143
+ let requests = state.requests;
1144
+ if (filter?.type) {
1145
+ requests = requests.filter((r) => r.type === filter.type);
1146
+ }
1147
+ if (filter?.method) {
1148
+ requests = requests.filter((r) => r.method === filter.method);
1149
+ }
1150
+ if (filter?.minDuration) {
1151
+ requests = requests.filter((r) => (r.duration ?? 0) >= (filter.minDuration ?? 0));
1152
+ }
1153
+ return requests;
1154
+ }, [state.requests, filter?.type, filter?.method, filter?.minDuration]);
1155
+ }
1156
+ function useFluxReport() {
1157
+ const { state } = useFlux();
1158
+ return state.report;
1159
+ }
1160
+ function useFluxScanning() {
1161
+ const { bridge, state } = useFlux();
1162
+ return (0, import_react4.useMemo)(
1163
+ () => ({
1164
+ scanning: state.scanning,
1165
+ elapsed: state.elapsed,
1166
+ requestCount: state.requests.length,
1167
+ start: () => bridge.start(),
1168
+ stop: () => bridge.stop(),
1169
+ reset: () => bridge.reset()
1170
+ }),
1171
+ [bridge, state.scanning, state.elapsed, state.requests.length]
1172
+ );
1173
+ }
1174
+
1175
+ // src/integrations/tanstack.ts
1176
+ function wrapQueryClient(queryClient, bridge) {
1177
+ const _bridge = bridge ?? getGlobalBridge();
1178
+ try {
1179
+ const qc = queryClient;
1180
+ if (typeof qc.getQueryCache === "function") {
1181
+ const cache = qc.getQueryCache();
1182
+ if (typeof cache.subscribe === "function") {
1183
+ cache.subscribe((event) => {
1184
+ if (!event.query) return;
1185
+ const { query } = event;
1186
+ const eventType = event.type;
1187
+ if (eventType === "added" || eventType === "updated") {
1188
+ _bridge.captureQueryEvent({
1189
+ type: eventType === "added" ? "query-added" : "query-updated",
1190
+ queryKey: query.queryKey,
1191
+ state: {
1192
+ status: query.state.status,
1193
+ fetchStatus: query.state.fetchStatus,
1194
+ dataUpdatedAt: query.state.dataUpdatedAt
1195
+ },
1196
+ options: {
1197
+ staleTime: query.options.staleTime,
1198
+ gcTime: query.options.gcTime,
1199
+ refetchInterval: query.options.refetchInterval,
1200
+ enabled: query.options.enabled,
1201
+ retry: query.options.retry
1202
+ }
1203
+ });
1204
+ } else if (eventType === "removed") {
1205
+ _bridge.captureQueryEvent({
1206
+ type: "query-removed",
1207
+ queryKey: query.queryKey
1208
+ });
1209
+ }
1210
+ });
1211
+ }
1212
+ }
1213
+ } catch (err) {
1214
+ console.warn("[FluxAPI] Failed to instrument QueryClient:", err);
1215
+ }
1216
+ return queryClient;
1217
+ }
1218
+ function extractQueryMetrics(queryClient) {
1219
+ const metrics = {
1220
+ uniqueQueries: 0,
1221
+ queriesWithoutStaleTime: 0,
1222
+ pollingQueries: 0,
1223
+ noRetryQueries: 0,
1224
+ avgStaleTime: 0
1225
+ };
1226
+ try {
1227
+ if (typeof queryClient.getQueryCache !== "function") return metrics;
1228
+ const cache = queryClient.getQueryCache();
1229
+ const queries = cache.getAll?.() ?? [];
1230
+ metrics.uniqueQueries = queries.length;
1231
+ let totalStaleTime = 0;
1232
+ let staleTimeCount = 0;
1233
+ queries.forEach((q) => {
1234
+ const opts = q.options ?? {};
1235
+ if (!opts.staleTime || opts.staleTime === 0) {
1236
+ metrics.queriesWithoutStaleTime++;
1237
+ } else {
1238
+ totalStaleTime += opts.staleTime;
1239
+ staleTimeCount++;
1240
+ }
1241
+ if (opts.refetchInterval && opts.refetchInterval !== false) {
1242
+ metrics.pollingQueries++;
1243
+ }
1244
+ if (opts.retry === false || opts.retry === 0) {
1245
+ metrics.noRetryQueries++;
1246
+ }
1247
+ });
1248
+ metrics.avgStaleTime = staleTimeCount > 0 ? Math.round(totalStaleTime / staleTimeCount) : 0;
1249
+ } catch {
1250
+ }
1251
+ return metrics;
1252
+ }
1253
+
1254
+ // src/integrations/swr.ts
1255
+ function createFluxSWRMiddleware(bridge) {
1256
+ const _bridge = bridge ?? getGlobalBridge();
1257
+ return (useSWRNext) => {
1258
+ return (key, fetcher, config) => {
1259
+ const resolvedKey = typeof key === "function" ? (() => {
1260
+ try {
1261
+ return key();
1262
+ } catch {
1263
+ return null;
1264
+ }
1265
+ })() : key;
1266
+ const keyStr = resolvedKey ? typeof resolvedKey === "string" ? resolvedKey : JSON.stringify(resolvedKey) : "null";
1267
+ _bridge.captureSWREvent({
1268
+ type: "swr-request",
1269
+ key: keyStr,
1270
+ config: {
1271
+ refreshInterval: config.refreshInterval,
1272
+ dedupingInterval: config.dedupingInterval,
1273
+ revalidateOnFocus: config.revalidateOnFocus,
1274
+ errorRetryCount: config.errorRetryCount
1275
+ }
1276
+ });
1277
+ const instrumentedFetcher = fetcher ? async (...args) => {
1278
+ const start = Date.now();
1279
+ try {
1280
+ const result = await fetcher(...args);
1281
+ _bridge.captureSWREvent({
1282
+ type: "swr-success",
1283
+ key: keyStr
1284
+ });
1285
+ return result;
1286
+ } catch (err) {
1287
+ _bridge.captureSWREvent({
1288
+ type: "swr-error",
1289
+ key: keyStr
1290
+ });
1291
+ throw err;
1292
+ }
1293
+ } : fetcher;
1294
+ return useSWRNext(key, instrumentedFetcher, config);
1295
+ };
1296
+ };
1297
+ }
1298
+ var fluxSWRMiddleware = createFluxSWRMiddleware();
1299
+ // Annotate the CommonJS export names for ESM import in node:
1300
+ 0 && (module.exports = {
1301
+ FluxDevTools,
1302
+ FluxProvider,
1303
+ ScannerBridge,
1304
+ createFluxSWRMiddleware,
1305
+ extractQueryMetrics,
1306
+ fluxSWRMiddleware,
1307
+ getGlobalBridge,
1308
+ resetGlobalBridge,
1309
+ useFlux,
1310
+ useFluxBridge,
1311
+ useFluxReport,
1312
+ useFluxRequests,
1313
+ useFluxScanning,
1314
+ useFluxScore,
1315
+ useFluxState,
1316
+ useFluxViolations,
1317
+ wrapQueryClient
1318
+ });