@geenius/tools 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/.changeset/config.json +11 -0
- package/.env.example +2 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc +7 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +16 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +18 -0
- package/SUPPORT.md +14 -0
- package/package.json +75 -0
- package/packages/convex/shared/README.md +1 -0
- package/packages/convex/shared/package.json +42 -0
- package/packages/convex/shared/src/audit/index.ts +5 -0
- package/packages/convex/shared/src/audit/presets.ts +165 -0
- package/packages/convex/shared/src/audit/schema.ts +85 -0
- package/packages/convex/shared/src/audit/write.ts +102 -0
- package/packages/convex/shared/src/extract.ts +75 -0
- package/packages/convex/shared/src/index.ts +41 -0
- package/packages/convex/shared/src/messages.ts +45 -0
- package/packages/convex/shared/src/security.ts +112 -0
- package/packages/convex/shared/src/throw.ts +184 -0
- package/packages/convex/shared/src/types.ts +57 -0
- package/packages/convex/shared/src/utils.ts +58 -0
- package/packages/convex/shared/tsconfig.json +28 -0
- package/packages/convex/shared/tsup.config.ts +12 -0
- package/packages/devtools/package.json +27 -0
- package/packages/devtools/react/README.md +1 -0
- package/packages/devtools/react/package.json +53 -0
- package/packages/devtools/react/src/components/DesignPreview.tsx +59 -0
- package/packages/devtools/react/src/components/DesignSwitcherDropdown.tsx +99 -0
- package/packages/devtools/react/src/components/DevSidebar.tsx +247 -0
- package/packages/devtools/react/src/components/DevToolbar.tsx +242 -0
- package/packages/devtools/react/src/components/GitHubIssueDialog.tsx +402 -0
- package/packages/devtools/react/src/components/InspectorOverlay.tsx +312 -0
- package/packages/devtools/react/src/components/PageLoadWaterfall.tsx +144 -0
- package/packages/devtools/react/src/components/PerformancePanel.tsx +330 -0
- package/packages/devtools/react/src/context/DevModeContext.tsx +226 -0
- package/packages/devtools/react/src/context/PerformanceContext.tsx +143 -0
- package/packages/devtools/react/src/data/designs.ts +13 -0
- package/packages/devtools/react/src/hooks/useGitHubLabels.ts +47 -0
- package/packages/devtools/react/src/hooks/useVirtualList.ts +124 -0
- package/packages/devtools/react/src/index.ts +77 -0
- package/packages/devtools/react/src/panels/ConvexSpy.tsx +130 -0
- package/packages/devtools/react/src/panels/DatabaseSeeder.tsx +116 -0
- package/packages/devtools/react/src/panels/DevModePhase2.tsx +191 -0
- package/packages/devtools/react/src/panels/DevModePhase3.tsx +234 -0
- package/packages/devtools/react/src/panels/FeatureFlagsToggle.tsx +104 -0
- package/packages/devtools/react/src/panels/QuickRouteJump.tsx +152 -0
- package/packages/devtools/react/src/services/github-service.ts +247 -0
- package/packages/devtools/react/tsconfig.json +31 -0
- package/packages/devtools/react/tsup.config.ts +18 -0
- package/packages/devtools/solidjs/README.md +1 -0
- package/packages/devtools/solidjs/package.json +49 -0
- package/packages/devtools/solidjs/src/components/DesignPreview.tsx +51 -0
- package/packages/devtools/solidjs/src/components/DesignSwitcherDropdown.tsx +95 -0
- package/packages/devtools/solidjs/src/components/DevSidebar.tsx +247 -0
- package/packages/devtools/solidjs/src/components/DevToolbar.tsx +242 -0
- package/packages/devtools/solidjs/src/components/GitHubIssueDialog.tsx +400 -0
- package/packages/devtools/solidjs/src/components/InspectorOverlay.tsx +311 -0
- package/packages/devtools/solidjs/src/components/PageLoadWaterfall.tsx +144 -0
- package/packages/devtools/solidjs/src/components/PerformancePanel.tsx +330 -0
- package/packages/devtools/solidjs/src/context/DevModeContext.tsx +216 -0
- package/packages/devtools/solidjs/src/context/PerformanceContext.tsx +135 -0
- package/packages/devtools/solidjs/src/data/designs.ts +13 -0
- package/packages/devtools/solidjs/src/hooks/createGitHubLabels.ts +47 -0
- package/packages/devtools/solidjs/src/index.ts +64 -0
- package/packages/devtools/solidjs/src/services/github-service.ts +247 -0
- package/packages/devtools/solidjs/tsconfig.json +21 -0
- package/packages/devtools/src/index.ts +377 -0
- package/packages/devtools/tsup.config.ts +12 -0
- package/packages/env/package.json +30 -0
- package/packages/env/src/index.ts +264 -0
- package/packages/env/tsup.config.ts +12 -0
- package/packages/errors/package.json +27 -0
- package/packages/errors/react/README.md +1 -0
- package/packages/errors/react/package.json +72 -0
- package/packages/errors/react/src/analytics.ts +16 -0
- package/packages/errors/react/src/components/ErrorBoundary.tsx +248 -0
- package/packages/errors/react/src/components/ErrorDisplay.tsx +328 -0
- package/packages/errors/react/src/components/ValidationErrors.tsx +102 -0
- package/packages/errors/react/src/config.ts +199 -0
- package/packages/errors/react/src/constants.ts +74 -0
- package/packages/errors/react/src/hooks/useErrorBoundary.ts +92 -0
- package/packages/errors/react/src/hooks/useErrorHandler.ts +87 -0
- package/packages/errors/react/src/index.ts +96 -0
- package/packages/errors/react/src/types.ts +102 -0
- package/packages/errors/react/src/utils/errorMessages.ts +35 -0
- package/packages/errors/react/src/utils/errorPolicy.ts +139 -0
- package/packages/errors/react/src/utils/extractAppError.ts +174 -0
- package/packages/errors/react/src/utils/formatError.ts +112 -0
- package/packages/errors/react/tsconfig.json +25 -0
- package/packages/errors/react/tsup.config.ts +24 -0
- package/packages/errors/solidjs/README.md +1 -0
- package/packages/errors/solidjs/package.json +46 -0
- package/packages/errors/solidjs/src/components/ErrorDisplay.tsx +179 -0
- package/packages/errors/solidjs/src/config.ts +98 -0
- package/packages/errors/solidjs/src/hooks/createErrorHandler.ts +107 -0
- package/packages/errors/solidjs/src/index.ts +61 -0
- package/packages/errors/solidjs/src/types.ts +34 -0
- package/packages/errors/solidjs/src/utils/errorPolicy.ts +56 -0
- package/packages/errors/solidjs/src/utils/extractAppError.ts +94 -0
- package/packages/errors/solidjs/src/utils/formatError.ts +33 -0
- package/packages/errors/solidjs/tsconfig.json +26 -0
- package/packages/errors/solidjs/tsup.config.ts +21 -0
- package/packages/errors/src/index.ts +320 -0
- package/packages/errors/tsup.config.ts +12 -0
- package/packages/logger/package.json +27 -0
- package/packages/logger/react/README.md +1 -0
- package/packages/logger/react/package.json +46 -0
- package/packages/logger/react/src/index.ts +4 -0
- package/packages/logger/react/src/useMetrics.ts +42 -0
- package/packages/logger/react/src/usePerformanceLog.ts +61 -0
- package/packages/logger/react/tsconfig.json +31 -0
- package/packages/logger/react/tsup.config.ts +12 -0
- package/packages/logger/solidjs/README.md +1 -0
- package/packages/logger/solidjs/package.json +45 -0
- package/packages/logger/solidjs/src/createMetrics.ts +37 -0
- package/packages/logger/solidjs/src/createPerformanceLog.ts +58 -0
- package/packages/logger/solidjs/src/index.ts +4 -0
- package/packages/logger/solidjs/tsconfig.json +32 -0
- package/packages/logger/solidjs/tsup.config.ts +12 -0
- package/packages/logger/src/index.ts +363 -0
- package/packages/logger/tsup.config.ts +12 -0
- package/packages/perf/package.json +27 -0
- package/packages/perf/react/README.md +1 -0
- package/packages/perf/react/package.json +59 -0
- package/packages/perf/react/src/components/PerformanceDashboard.tsx +257 -0
- package/packages/perf/react/src/hooks/useMonitoredQuery.ts +89 -0
- package/packages/perf/react/src/hooks/usePerformanceMetrics.ts +78 -0
- package/packages/perf/react/src/index.ts +33 -0
- package/packages/perf/react/src/services/PerformanceMonitor.ts +313 -0
- package/packages/perf/react/src/types.ts +77 -0
- package/packages/perf/react/tsconfig.json +25 -0
- package/packages/perf/react/tsup.config.ts +19 -0
- package/packages/perf/solidjs/README.md +1 -0
- package/packages/perf/solidjs/package.json +41 -0
- package/packages/perf/solidjs/src/components/PerformanceDashboard.tsx +207 -0
- package/packages/perf/solidjs/src/hooks/createPerformanceMetrics.ts +73 -0
- package/packages/perf/solidjs/src/index.ts +31 -0
- package/packages/perf/solidjs/src/services/PerformanceMonitor.ts +134 -0
- package/packages/perf/solidjs/src/types.ts +78 -0
- package/packages/perf/solidjs/tsconfig.json +26 -0
- package/packages/perf/solidjs/tsup.config.ts +14 -0
- package/packages/perf/src/index.ts +410 -0
- package/packages/perf/tsup.config.ts +12 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// @geenius-tools/devtools-react — src/components/InspectorOverlay.tsx
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState, useRef } from 'react'
|
|
4
|
+
import { createPortal } from 'react-dom'
|
|
5
|
+
import { useDevModeOptional } from '../context/DevModeContext'
|
|
6
|
+
import { cn } from '@geenius-ui/react'
|
|
7
|
+
|
|
8
|
+
interface Rect {
|
|
9
|
+
top: number
|
|
10
|
+
left: number
|
|
11
|
+
width: number
|
|
12
|
+
height: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ComponentInfo {
|
|
16
|
+
name: string
|
|
17
|
+
file?: string
|
|
18
|
+
key?: string
|
|
19
|
+
props?: Record<string, unknown>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getComponentInfo(element: HTMLElement): ComponentInfo | null {
|
|
23
|
+
let fiberNode: any = null
|
|
24
|
+
const key = Object.keys(element).find((k) => k.startsWith('__reactFiber$'))
|
|
25
|
+
if (key) {
|
|
26
|
+
fiberNode = (element as any)[key]
|
|
27
|
+
}
|
|
28
|
+
if (!fiberNode) return null
|
|
29
|
+
|
|
30
|
+
const elementKey = fiberNode.key !== null ? String(fiberNode.key) : undefined
|
|
31
|
+
|
|
32
|
+
let node = fiberNode
|
|
33
|
+
while (node) {
|
|
34
|
+
if (node.tag === 0 || node.tag === 1) {
|
|
35
|
+
if (node.type && (node.type.displayName || node.type.name)) {
|
|
36
|
+
return {
|
|
37
|
+
name: node.type.displayName || node.type.name,
|
|
38
|
+
file: node._debugSource?.fileName,
|
|
39
|
+
key:
|
|
40
|
+
elementKey !== undefined
|
|
41
|
+
? elementKey
|
|
42
|
+
: node.key !== null
|
|
43
|
+
? String(node.key)
|
|
44
|
+
: undefined,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
node = node.return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { name: element.tagName.toLowerCase() }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function InspectorOverlay() {
|
|
55
|
+
const devMode = useDevModeOptional()
|
|
56
|
+
const [hoveredRect, setHoveredRect] = useState<Rect | null>(null)
|
|
57
|
+
const [selectedComponents, setSelectedComponents] = useState<
|
|
58
|
+
{ rect: Rect; info: ComponentInfo }[]
|
|
59
|
+
>([])
|
|
60
|
+
const [hoveredComponent, setHoveredComponent] =
|
|
61
|
+
useState<ComponentInfo | null>(null)
|
|
62
|
+
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 })
|
|
63
|
+
const overlayRef = useRef<HTMLDivElement>(null)
|
|
64
|
+
|
|
65
|
+
const isInspectorMode = devMode?.isInspectorMode ?? false
|
|
66
|
+
const setInspectorMode = devMode?.setInspectorMode ?? (() => { })
|
|
67
|
+
const openIssueDialog = devMode?.openIssueDialog ?? (() => { })
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!isInspectorMode) {
|
|
71
|
+
setHoveredRect(null)
|
|
72
|
+
setSelectedComponents([])
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isDevTool = (element: Element | null) => {
|
|
77
|
+
if (!element) return false
|
|
78
|
+
return !!element.closest('[data-dev-tool="true"]')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
82
|
+
setCursorPos({ x: e.clientX, y: e.clientY })
|
|
83
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY)
|
|
84
|
+
|
|
85
|
+
if (elements.length > 0 && isDevTool(elements[0])) {
|
|
86
|
+
setHoveredRect(null)
|
|
87
|
+
setHoveredComponent(null)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let target: HTMLElement | null = null
|
|
92
|
+
for (const el of elements) {
|
|
93
|
+
if (!isDevTool(el)) {
|
|
94
|
+
target = el as HTMLElement
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
target &&
|
|
101
|
+
target !== document.body &&
|
|
102
|
+
target !== document.documentElement
|
|
103
|
+
) {
|
|
104
|
+
const rect = target.getBoundingClientRect()
|
|
105
|
+
setHoveredRect({
|
|
106
|
+
top: rect.top,
|
|
107
|
+
left: rect.left,
|
|
108
|
+
width: rect.width,
|
|
109
|
+
height: rect.height,
|
|
110
|
+
})
|
|
111
|
+
setHoveredComponent(getComponentInfo(target))
|
|
112
|
+
} else {
|
|
113
|
+
setHoveredRect(null)
|
|
114
|
+
setHoveredComponent(null)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleClick = (e: MouseEvent) => {
|
|
119
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY)
|
|
120
|
+
if (elements.length > 0 && isDevTool(elements[0])) return
|
|
121
|
+
|
|
122
|
+
let target: HTMLElement | null = null
|
|
123
|
+
for (const el of elements) {
|
|
124
|
+
if (!isDevTool(el)) {
|
|
125
|
+
target = el as HTMLElement
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (target) {
|
|
131
|
+
e.preventDefault()
|
|
132
|
+
e.stopPropagation()
|
|
133
|
+
|
|
134
|
+
const info = getComponentInfo(target)
|
|
135
|
+
const rect = target.getBoundingClientRect()
|
|
136
|
+
|
|
137
|
+
if (info) {
|
|
138
|
+
setSelectedComponents((prev) => {
|
|
139
|
+
const existsIndex = prev.findIndex(
|
|
140
|
+
(p) =>
|
|
141
|
+
p.info.name === info.name &&
|
|
142
|
+
p.info.key === info.key &&
|
|
143
|
+
p.info.file === info.file,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if (existsIndex >= 0) {
|
|
147
|
+
const newSelection = [...prev]
|
|
148
|
+
newSelection.splice(existsIndex, 1)
|
|
149
|
+
return newSelection
|
|
150
|
+
} else {
|
|
151
|
+
return [
|
|
152
|
+
...prev,
|
|
153
|
+
{
|
|
154
|
+
rect: {
|
|
155
|
+
top: rect.top,
|
|
156
|
+
left: rect.left,
|
|
157
|
+
width: rect.width,
|
|
158
|
+
height: rect.height,
|
|
159
|
+
},
|
|
160
|
+
info,
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
window.addEventListener('mousemove', handleMouseMove, { capture: true })
|
|
170
|
+
window.addEventListener('click', handleClick, { capture: true })
|
|
171
|
+
|
|
172
|
+
return () => {
|
|
173
|
+
window.removeEventListener('mousemove', handleMouseMove, { capture: true })
|
|
174
|
+
window.removeEventListener('click', handleClick, { capture: true })
|
|
175
|
+
}
|
|
176
|
+
}, [isInspectorMode, hoveredRect, hoveredComponent, openIssueDialog, setInspectorMode])
|
|
177
|
+
|
|
178
|
+
if (!devMode || !isInspectorMode) return null
|
|
179
|
+
|
|
180
|
+
return createPortal(
|
|
181
|
+
<div
|
|
182
|
+
ref={overlayRef}
|
|
183
|
+
className="fixed inset-0 z-[9999] pointer-events-none cursor-crosshair"
|
|
184
|
+
style={{ background: 'transparent' }}
|
|
185
|
+
data-dev-tool="true"
|
|
186
|
+
>
|
|
187
|
+
{/* Desktop Debug Panel */}
|
|
188
|
+
<div className="hidden md:block fixed top-0 left-0 z-[10000] p-2 text-xs font-mono text-white pointer-events-auto transition-all duration-300">
|
|
189
|
+
<div
|
|
190
|
+
className={cn(
|
|
191
|
+
'group flex flex-col gap-1 min-w-[150px] p-2 rounded-lg backdrop-blur-sm transition-all duration-300',
|
|
192
|
+
'bg-blue-500/10 hover:bg-blue-950/90',
|
|
193
|
+
'border border-blue-500/30',
|
|
194
|
+
'text-black hover:text-white',
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
197
|
+
<div className="flex justify-between items-center opacity-80">
|
|
198
|
+
<span className="font-bold opacity-100 group-hover:text-white/90">
|
|
199
|
+
INSPECTOR
|
|
200
|
+
</span>
|
|
201
|
+
<span className="opacity-70 group-hover:text-white/70">
|
|
202
|
+
{Math.round(cursorPos.x)}, {Math.round(cursorPos.y)}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
{hoveredComponent && (
|
|
206
|
+
<div className="font-bold drop-shadow-sm truncate text-blue-700 group-hover:text-blue-300">
|
|
207
|
+
{hoveredComponent.name}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
{selectedComponents.length > 0 && (
|
|
211
|
+
<div className="truncate text-[10px] opacity-80 text-green-700 group-hover:text-green-300 border-t border-white/10 pt-1 mt-1">
|
|
212
|
+
{selectedComponents.length} Selected
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Hover Highlight */}
|
|
219
|
+
{hoveredRect && (
|
|
220
|
+
<div
|
|
221
|
+
className="absolute border-2 border-blue-500 bg-blue-500/10 pointer-events-none transition-all duration-75 ease-out"
|
|
222
|
+
style={{
|
|
223
|
+
top: hoveredRect.top,
|
|
224
|
+
left: hoveredRect.left,
|
|
225
|
+
width: hoveredRect.width,
|
|
226
|
+
height: hoveredRect.height,
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<div className="absolute -top-6 left-0 bg-blue-500 text-white text-[10px] px-1.5 py-0.5 rounded-sm whitespace-nowrap">
|
|
230
|
+
{hoveredComponent?.name} ({Math.round(hoveredRect.width)} x{' '}
|
|
231
|
+
{Math.round(hoveredRect.height)})
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Selection Highlights */}
|
|
237
|
+
{selectedComponents.map((comp, i) => (
|
|
238
|
+
<div
|
|
239
|
+
key={i}
|
|
240
|
+
className="absolute border-2 border-green-500 bg-green-500/20 pointer-events-none transition-all duration-75 ease-out"
|
|
241
|
+
style={{
|
|
242
|
+
top: comp.rect.top,
|
|
243
|
+
left: comp.rect.left,
|
|
244
|
+
width: comp.rect.width,
|
|
245
|
+
height: comp.rect.height,
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<div className="absolute -top-6 left-0 bg-green-600 text-white text-[10px] px-1.5 py-0.5 rounded-sm whitespace-nowrap z-[10001]">
|
|
249
|
+
{comp.info.name}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
))}
|
|
253
|
+
|
|
254
|
+
{/* Floating Toolbar */}
|
|
255
|
+
{selectedComponents.length > 0 &&
|
|
256
|
+
(() => {
|
|
257
|
+
const lastSelected = selectedComponents[selectedComponents.length - 1]
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
className="fixed z-[10002] flex items-center gap-1 p-1 bg-slate-900 border border-slate-700 rounded-md shadow-xl animate-in fade-in zoom-in-95 pointer-events-auto"
|
|
261
|
+
style={{
|
|
262
|
+
top: Math.max(10, lastSelected.rect.top - 45),
|
|
263
|
+
left: Math.max(10, lastSelected.rect.left),
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => {
|
|
268
|
+
const allDetails = selectedComponents.map((c) => ({
|
|
269
|
+
name: c.info.name,
|
|
270
|
+
file: c.info.file,
|
|
271
|
+
key: c.info.key,
|
|
272
|
+
props: c.info.props,
|
|
273
|
+
selector: 'Multi-select',
|
|
274
|
+
}))
|
|
275
|
+
openIssueDialog('feature', '', allDetails as any)
|
|
276
|
+
setInspectorMode(false)
|
|
277
|
+
}}
|
|
278
|
+
className="px-2 py-1 text-xs font-medium text-white hover:bg-white/10 rounded transition-colors"
|
|
279
|
+
>
|
|
280
|
+
Feature
|
|
281
|
+
</button>
|
|
282
|
+
<div className="w-px h-3 bg-white/20 mx-0.5" />
|
|
283
|
+
<button
|
|
284
|
+
onClick={() => {
|
|
285
|
+
const allDetails = selectedComponents.map((c) => ({
|
|
286
|
+
name: c.info.name,
|
|
287
|
+
file: c.info.file,
|
|
288
|
+
key: c.info.key,
|
|
289
|
+
props: c.info.props,
|
|
290
|
+
selector: 'Multi-select',
|
|
291
|
+
}))
|
|
292
|
+
openIssueDialog('bug', '', allDetails as any)
|
|
293
|
+
setInspectorMode(false)
|
|
294
|
+
}}
|
|
295
|
+
className="px-2 py-1 text-xs font-medium text-red-400 hover:bg-red-500/10 rounded transition-colors"
|
|
296
|
+
>
|
|
297
|
+
Bug
|
|
298
|
+
</button>
|
|
299
|
+
<div className="w-px h-3 bg-white/20 mx-0.5" />
|
|
300
|
+
<button
|
|
301
|
+
onClick={() => setSelectedComponents([])}
|
|
302
|
+
className="px-2 py-1 text-xs text-slate-400 hover:text-white hover:bg-white/10 rounded transition-colors"
|
|
303
|
+
>
|
|
304
|
+
Deselect all
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
)
|
|
308
|
+
})()}
|
|
309
|
+
</div>,
|
|
310
|
+
document.body,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @geenius-tools/devtools-react — components/PageLoadWaterfall.tsx
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
export type SsrTiming = {
|
|
5
|
+
ssrStart: number
|
|
6
|
+
getAuthStart: number
|
|
7
|
+
getAuthEnd: number
|
|
8
|
+
translationsStart: number
|
|
9
|
+
translationsEnd: number
|
|
10
|
+
userPrefsStart: number
|
|
11
|
+
userPrefsEnd: number
|
|
12
|
+
ssrEnd: number
|
|
13
|
+
isCachedAuth: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* PageLoadWaterfall — invisible component that logs detailed performance
|
|
18
|
+
* waterfall data to the browser console on first mount.
|
|
19
|
+
*
|
|
20
|
+
* Includes: network timing, SSR breakdown, FCP, resource loading, top-5 slowest JS bundles.
|
|
21
|
+
* Returns `null` — no UI.
|
|
22
|
+
*/
|
|
23
|
+
export function PageLoadWaterfall({ ssrTiming }: { ssrTiming?: SsrTiming | null }) {
|
|
24
|
+
const hasLogged = React.useRef(false)
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (hasLogged.current) return
|
|
28
|
+
hasLogged.current = true
|
|
29
|
+
logWaterfall(ssrTiming)
|
|
30
|
+
}, [ssrTiming])
|
|
31
|
+
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @internal Shared waterfall logging logic — framework-agnostic */
|
|
36
|
+
export function logWaterfall(ssrTiming?: SsrTiming | null) {
|
|
37
|
+
const clientStart = performance.now()
|
|
38
|
+
|
|
39
|
+
const run = () => {
|
|
40
|
+
const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined
|
|
41
|
+
const fcp = performance.getEntriesByType('paint').find((e) => e.name === 'first-contentful-paint')
|
|
42
|
+
|
|
43
|
+
console.log('\n')
|
|
44
|
+
console.log('%c╔══════════════════════════════════════════════════════╗', 'color: #4CAF50; font-weight: bold')
|
|
45
|
+
console.log('%c║ PAGE LOAD PERFORMANCE WATERFALL ║', 'color: #4CAF50; font-weight: bold')
|
|
46
|
+
console.log('%c╚══════════════════════════════════════════════════════╝', 'color: #4CAF50; font-weight: bold')
|
|
47
|
+
|
|
48
|
+
if (navEntry) {
|
|
49
|
+
const dns = navEntry.domainLookupEnd - navEntry.domainLookupStart
|
|
50
|
+
const tcp = navEntry.connectEnd - navEntry.connectStart
|
|
51
|
+
const ttfb = navEntry.responseStart - navEntry.requestStart
|
|
52
|
+
const download = navEntry.responseEnd - navEntry.responseStart
|
|
53
|
+
const domParsing = navEntry.domInteractive - navEntry.responseEnd
|
|
54
|
+
const domComplete = navEntry.domComplete - navEntry.domInteractive
|
|
55
|
+
const totalLoad = navEntry.loadEventEnd - navEntry.startTime
|
|
56
|
+
|
|
57
|
+
console.log('%c\n── Network & Browser Timing ──', 'color: #FF9800; font-weight: bold')
|
|
58
|
+
console.table({
|
|
59
|
+
'DNS Lookup': { ms: Math.round(dns), bar: '█'.repeat(Math.max(1, Math.round(dns / 20))) },
|
|
60
|
+
'TCP Connect': { ms: Math.round(tcp), bar: '█'.repeat(Math.max(1, Math.round(tcp / 20))) },
|
|
61
|
+
'TTFB (Time to First Byte)': { ms: Math.round(ttfb), bar: '█'.repeat(Math.max(1, Math.round(ttfb / 20))) },
|
|
62
|
+
'Response Download': { ms: Math.round(download), bar: '█'.repeat(Math.max(1, Math.round(download / 20))) },
|
|
63
|
+
'DOM Parsing': { ms: Math.round(domParsing), bar: '█'.repeat(Math.max(1, Math.round(domParsing / 20))) },
|
|
64
|
+
'DOM Complete': { ms: Math.round(domComplete), bar: '█'.repeat(Math.max(1, Math.round(domComplete / 20))) },
|
|
65
|
+
'Total Page Load': { ms: Math.round(totalLoad), bar: '█'.repeat(Math.max(1, Math.round(totalLoad / 20))) },
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
console.log(
|
|
69
|
+
`%c\n⚡ TTFB: ${Math.round(ttfb)}ms — This includes all SSR processing time`,
|
|
70
|
+
ttfb > 500 ? 'color: #F44336; font-weight: bold; font-size: 14px' : 'color: #4CAF50; font-weight: bold; font-size: 14px',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (ssrTiming) {
|
|
75
|
+
console.log('%c\n── SSR beforeLoad Breakdown ──', 'color: #2196F3; font-weight: bold')
|
|
76
|
+
const total = ssrTiming.ssrEnd - ssrTiming.ssrStart
|
|
77
|
+
const auth = ssrTiming.getAuthEnd ? ssrTiming.getAuthEnd - ssrTiming.getAuthStart : 0
|
|
78
|
+
const prefs = ssrTiming.userPrefsEnd ? ssrTiming.userPrefsEnd - ssrTiming.userPrefsStart : 0
|
|
79
|
+
const trans = ssrTiming.translationsEnd ? ssrTiming.translationsEnd - ssrTiming.translationsStart : 0
|
|
80
|
+
|
|
81
|
+
console.table({
|
|
82
|
+
'getAuth()': { ms: auth, bar: '█'.repeat(Math.max(1, Math.round(auth / 20))), note: ssrTiming.isCachedAuth ? '(cached)' : '(server call)' },
|
|
83
|
+
'User Prefs': { ms: prefs, bar: '█'.repeat(Math.max(1, Math.round(prefs / 20))) },
|
|
84
|
+
'loadTranslations()': { ms: trans, bar: '█'.repeat(Math.max(1, Math.round(trans / 20))) },
|
|
85
|
+
'Total SSR beforeLoad': { ms: total, bar: '█'.repeat(Math.max(1, Math.round(total / 20))) },
|
|
86
|
+
})
|
|
87
|
+
} else {
|
|
88
|
+
console.log('%c\n── SSR Timing: Not available (client-side navigation) ──', 'color: #9E9E9E')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (fcp) {
|
|
92
|
+
console.log(
|
|
93
|
+
`%c\n🎨 First Contentful Paint: ${Math.round(fcp.startTime)}ms`,
|
|
94
|
+
fcp.startTime > 1500 ? 'color: #F44336; font-weight: bold; font-size: 14px' : 'color: #4CAF50; font-weight: bold; font-size: 14px',
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`%c\n⚛️ Component mounted at: ${Math.round(clientStart)}ms from page start`,
|
|
100
|
+
'color: #61DAFB; font-weight: bold',
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
|
104
|
+
const jsResources = resources.filter((r) => r.name.endsWith('.js') || r.name.endsWith('.mjs') || r.name.includes('.js?'))
|
|
105
|
+
const cssResources = resources.filter((r) => r.name.endsWith('.css') || r.name.includes('.css?'))
|
|
106
|
+
const fontResources = resources.filter((r) => r.name.includes('fonts.googleapis') || r.name.includes('fonts.gstatic'))
|
|
107
|
+
const imgResources = resources.filter((r) => r.initiatorType === 'img')
|
|
108
|
+
|
|
109
|
+
const totalJsSize = jsResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
|
|
110
|
+
const slowestJs = [...jsResources].sort((a, b) => b.duration - a.duration).slice(0, 5)
|
|
111
|
+
|
|
112
|
+
console.log('%c\n── Resource Loading ──', 'color: #9C27B0; font-weight: bold')
|
|
113
|
+
console.table({
|
|
114
|
+
'JS bundles': { count: jsResources.length, totalKB: Math.round(totalJsSize / 1024), slowest_ms: Math.round(slowestJs[0]?.duration ?? 0) },
|
|
115
|
+
'CSS files': { count: cssResources.length, totalKB: Math.round(cssResources.reduce((s, r) => s + (r.transferSize || 0), 0) / 1024) },
|
|
116
|
+
'Font resources': { count: fontResources.length, slowest_ms: Math.round([...fontResources].sort((a, b) => b.duration - a.duration)[0]?.duration ?? 0) },
|
|
117
|
+
'Images': { count: imgResources.length, totalKB: Math.round(imgResources.reduce((s, r) => s + (r.transferSize || 0), 0) / 1024) },
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (slowestJs.length > 0) {
|
|
121
|
+
console.log('%c\n── Top 5 Slowest JS Bundles ──', 'color: #E91E63; font-weight: bold')
|
|
122
|
+
console.table(
|
|
123
|
+
Object.fromEntries(
|
|
124
|
+
slowestJs.map((r, i) => [
|
|
125
|
+
`#${i + 1}`,
|
|
126
|
+
{
|
|
127
|
+
file: r.name.split('/').pop()?.split('?')[0] ?? r.name,
|
|
128
|
+
duration_ms: Math.round(r.duration),
|
|
129
|
+
size_KB: Math.round((r.transferSize || 0) / 1024),
|
|
130
|
+
},
|
|
131
|
+
]),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log('%c\n══════════════════════════════════════════════════════', 'color: #4CAF50; font-weight: bold')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (document.readyState === 'complete') {
|
|
140
|
+
setTimeout(run, 500)
|
|
141
|
+
} else {
|
|
142
|
+
window.addEventListener('load', () => setTimeout(run, 500))
|
|
143
|
+
}
|
|
144
|
+
}
|