lynx-pretext-devtools 0.0.1
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/package.json +36 -0
- package/src/components/DevPanelActions.tsx +18 -0
- package/src/components/DevPanelButton.tsx +34 -0
- package/src/components/DevPanelContent.tsx +53 -0
- package/src/components/DevPanelFPS.tsx +56 -0
- package/src/components/DevPanelRoot.tsx +13 -0
- package/src/components/DevPanelStat.tsx +16 -0
- package/src/components/DevPanelStats.tsx +18 -0
- package/src/components/DevPanelStepper.tsx +82 -0
- package/src/components/DevPanelTrigger.tsx +46 -0
- package/src/constants.ts +40 -0
- package/src/context.tsx +30 -0
- package/src/hooks/useDevPanelFPS.ts +79 -0
- package/src/index.ts +71 -0
- package/src/types.ts +47 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lynx-pretext-devtools",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Developer tools and debugging components for lynx-pretext",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Huxpro",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Huxpro/lynx-pretext.git",
|
|
10
|
+
"directory": "packages/lynx-pretext-devtools"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"typecheck": "tsc --build"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"files": ["src"],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"lynx",
|
|
22
|
+
"devtools",
|
|
23
|
+
"debug",
|
|
24
|
+
"performance"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@lynx-js/react": ">=0.116.0",
|
|
28
|
+
"@lynx-js/types": ">=3.7.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@lynx-js/react": "0.116.4",
|
|
32
|
+
"@lynx-js/types": "3.7.0",
|
|
33
|
+
"@types/react": "^18.3.28",
|
|
34
|
+
"typescript": "~5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DevPanelActionsProps } from '../types'
|
|
2
|
+
|
|
3
|
+
export function DevPanelActions({
|
|
4
|
+
children,
|
|
5
|
+
}: DevPanelActionsProps): React.ReactElement {
|
|
6
|
+
return (
|
|
7
|
+
<view
|
|
8
|
+
style={{
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'row',
|
|
11
|
+
marginTop: '6px',
|
|
12
|
+
gap: '6px',
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</view>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useCallback } from '@lynx-js/react'
|
|
2
|
+
import { TEXT_PRIMARY, BUTTON_BG, BUTTON_ACTIVE, STEPPER_RADIUS } from '../constants'
|
|
3
|
+
import type { DevPanelButtonProps } from '../types'
|
|
4
|
+
|
|
5
|
+
export function DevPanelButton({
|
|
6
|
+
label,
|
|
7
|
+
active = false,
|
|
8
|
+
onPress,
|
|
9
|
+
}: DevPanelButtonProps): React.ReactElement {
|
|
10
|
+
const handlePress = useCallback(() => {
|
|
11
|
+
onPress()
|
|
12
|
+
}, [onPress])
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<view
|
|
16
|
+
bindtap={handlePress}
|
|
17
|
+
style={{
|
|
18
|
+
display: 'flex',
|
|
19
|
+
flexDirection: 'row',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
height: '22px',
|
|
23
|
+
paddingLeft: '8px',
|
|
24
|
+
paddingRight: '8px',
|
|
25
|
+
borderRadius: STEPPER_RADIUS,
|
|
26
|
+
backgroundColor: active ? BUTTON_ACTIVE : BUTTON_BG,
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<text style={{ fontSize: '10px', color: TEXT_PRIMARY, fontFamily: 'monospace' }}>
|
|
30
|
+
{label}
|
|
31
|
+
</text>
|
|
32
|
+
</view>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useDevPanelContext } from '../context'
|
|
2
|
+
import { PANEL_BG, PANEL_RADIUS, PANEL_PADDING, TEXT_PRIMARY, TEXT_SECONDARY, PANEL_TOP } from '../constants'
|
|
3
|
+
import type { DevPanelContentProps } from '../types'
|
|
4
|
+
|
|
5
|
+
export function DevPanelContent({
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
children,
|
|
9
|
+
}: DevPanelContentProps): React.ReactElement | null {
|
|
10
|
+
const { open } = useDevPanelContext()
|
|
11
|
+
|
|
12
|
+
if (!open) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<view
|
|
16
|
+
style={{
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
top: PANEL_TOP,
|
|
19
|
+
right: '8px',
|
|
20
|
+
minWidth: '140px',
|
|
21
|
+
backgroundColor: PANEL_BG,
|
|
22
|
+
borderRadius: PANEL_RADIUS,
|
|
23
|
+
padding: PANEL_PADDING,
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{title && (
|
|
27
|
+
<text style={{
|
|
28
|
+
fontSize: '10px',
|
|
29
|
+
color: TEXT_SECONDARY,
|
|
30
|
+
fontFamily: 'monospace',
|
|
31
|
+
textTransform: 'uppercase',
|
|
32
|
+
letterSpacing: '0.5px',
|
|
33
|
+
marginBottom: '6px',
|
|
34
|
+
}}>
|
|
35
|
+
{title}
|
|
36
|
+
</text>
|
|
37
|
+
)}
|
|
38
|
+
{description && (
|
|
39
|
+
<text
|
|
40
|
+
style={{
|
|
41
|
+
fontSize: '10px',
|
|
42
|
+
color: TEXT_SECONDARY,
|
|
43
|
+
marginTop: '2px',
|
|
44
|
+
lineHeight: '14px',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{description}
|
|
48
|
+
</text>
|
|
49
|
+
)}
|
|
50
|
+
{children}
|
|
51
|
+
</view>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { MainThreadRef } from '@lynx-js/react'
|
|
2
|
+
import type { MainThread } from '@lynx-js/types'
|
|
3
|
+
import { getFpsColor, TEXT_TERTIARY, TEXT_SECONDARY } from '../constants'
|
|
4
|
+
|
|
5
|
+
export interface DevPanelFPSProps {
|
|
6
|
+
mtsFpsDisplay: number
|
|
7
|
+
btsFpsDisplay: number
|
|
8
|
+
mtsFpsTextRef?: MainThreadRef<MainThread.Element | null>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DevPanelFPS({
|
|
12
|
+
mtsFpsDisplay,
|
|
13
|
+
btsFpsDisplay,
|
|
14
|
+
mtsFpsTextRef,
|
|
15
|
+
}: DevPanelFPSProps): React.ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<view
|
|
18
|
+
style={{
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'row',
|
|
21
|
+
marginTop: '6px',
|
|
22
|
+
gap: '12px',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{/* MTS FPS */}
|
|
27
|
+
<view style={{ display: 'flex', flexDirection: 'row', alignItems: 'baseline', gap: '3px' }}>
|
|
28
|
+
<text style={{ fontSize: '9px', color: TEXT_TERTIARY, fontFamily: 'monospace' }}>M</text>
|
|
29
|
+
<text
|
|
30
|
+
main-thread:ref={mtsFpsTextRef}
|
|
31
|
+
style={{
|
|
32
|
+
fontSize: '11px',
|
|
33
|
+
fontFamily: 'monospace',
|
|
34
|
+
color: getFpsColor(mtsFpsDisplay),
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{`${mtsFpsDisplay}`}
|
|
38
|
+
</text>
|
|
39
|
+
</view>
|
|
40
|
+
|
|
41
|
+
{/* BTS FPS */}
|
|
42
|
+
<view style={{ display: 'flex', flexDirection: 'row', alignItems: 'baseline', gap: '3px' }}>
|
|
43
|
+
<text style={{ fontSize: '9px', color: TEXT_TERTIARY, fontFamily: 'monospace' }}>B</text>
|
|
44
|
+
<text
|
|
45
|
+
style={{
|
|
46
|
+
fontSize: '11px',
|
|
47
|
+
fontFamily: 'monospace',
|
|
48
|
+
color: getFpsColor(btsFpsDisplay),
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{`${btsFpsDisplay}`}
|
|
52
|
+
</text>
|
|
53
|
+
</view>
|
|
54
|
+
</view>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DevPanelProvider } from '../context'
|
|
2
|
+
import type { DevPanelRootProps } from '../types'
|
|
3
|
+
|
|
4
|
+
export function DevPanelRoot({
|
|
5
|
+
children,
|
|
6
|
+
defaultOpen = false,
|
|
7
|
+
}: DevPanelRootProps): React.ReactElement {
|
|
8
|
+
return (
|
|
9
|
+
<DevPanelProvider defaultOpen={defaultOpen}>
|
|
10
|
+
{children}
|
|
11
|
+
</DevPanelProvider>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TEXT_PRIMARY, TEXT_TERTIARY } from '../constants'
|
|
2
|
+
import type { DevPanelStatProps } from '../types'
|
|
3
|
+
|
|
4
|
+
export function DevPanelStat({
|
|
5
|
+
label,
|
|
6
|
+
value,
|
|
7
|
+
}: DevPanelStatProps): React.ReactElement {
|
|
8
|
+
return (
|
|
9
|
+
<view style={{ display: 'flex', flexDirection: 'row', alignItems: 'baseline', gap: '4px' }}>
|
|
10
|
+
<text style={{ fontSize: '9px', color: TEXT_TERTIARY, fontFamily: 'monospace' }}>{label}</text>
|
|
11
|
+
<text style={{ fontSize: '11px', fontFamily: 'monospace', color: TEXT_PRIMARY }}>
|
|
12
|
+
{`${value}`}
|
|
13
|
+
</text>
|
|
14
|
+
</view>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DevPanelStatsProps } from '../types'
|
|
2
|
+
|
|
3
|
+
export function DevPanelStats({
|
|
4
|
+
children,
|
|
5
|
+
}: DevPanelStatsProps): React.ReactElement {
|
|
6
|
+
return (
|
|
7
|
+
<view
|
|
8
|
+
style={{
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'row',
|
|
11
|
+
gap: '10px',
|
|
12
|
+
marginTop: '6px',
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</view>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback } from '@lynx-js/react'
|
|
2
|
+
import {
|
|
3
|
+
STEPPER_BG,
|
|
4
|
+
STEPPER_SIZE,
|
|
5
|
+
STEPPER_RADIUS,
|
|
6
|
+
TEXT_PRIMARY,
|
|
7
|
+
TEXT_SECONDARY,
|
|
8
|
+
TEXT_TERTIARY,
|
|
9
|
+
} from '../constants'
|
|
10
|
+
import type { DevPanelStepperProps } from '../types'
|
|
11
|
+
|
|
12
|
+
export function DevPanelStepper({
|
|
13
|
+
label,
|
|
14
|
+
value,
|
|
15
|
+
min = -Infinity,
|
|
16
|
+
max = Infinity,
|
|
17
|
+
step = 1,
|
|
18
|
+
unit = 'px',
|
|
19
|
+
onChange,
|
|
20
|
+
}: DevPanelStepperProps): React.ReactElement {
|
|
21
|
+
const canDecrease = value > min
|
|
22
|
+
const canIncrease = value < max
|
|
23
|
+
|
|
24
|
+
const decrease = useCallback(() => {
|
|
25
|
+
if (canDecrease) {
|
|
26
|
+
onChange(Math.max(min, value - step))
|
|
27
|
+
}
|
|
28
|
+
}, [value, min, step, onChange, canDecrease])
|
|
29
|
+
|
|
30
|
+
const increase = useCallback(() => {
|
|
31
|
+
if (canIncrease) {
|
|
32
|
+
onChange(Math.min(max, value + step))
|
|
33
|
+
}
|
|
34
|
+
}, [value, max, step, onChange, canIncrease])
|
|
35
|
+
|
|
36
|
+
const buttonStyle = (enabled: boolean) => ({
|
|
37
|
+
width: STEPPER_SIZE,
|
|
38
|
+
height: STEPPER_SIZE,
|
|
39
|
+
borderRadius: STEPPER_RADIUS,
|
|
40
|
+
backgroundColor: enabled ? STEPPER_BG : 'transparent',
|
|
41
|
+
alignItems: 'center' as const,
|
|
42
|
+
justifyContent: 'center' as const,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<view
|
|
47
|
+
style={{
|
|
48
|
+
display: 'flex',
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
marginTop: '6px',
|
|
52
|
+
gap: '6px',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<text style={{ fontSize: '9px', color: TEXT_SECONDARY, fontFamily: 'monospace' }}>{label}</text>
|
|
56
|
+
|
|
57
|
+
<view bindtap={decrease} style={buttonStyle(canDecrease)}>
|
|
58
|
+
<text style={{ fontSize: '12px', color: canDecrease ? TEXT_PRIMARY : TEXT_TERTIARY, fontFamily: 'monospace', lineHeight: '14px' }}>
|
|
59
|
+
{'\u2212'}
|
|
60
|
+
</text>
|
|
61
|
+
</view>
|
|
62
|
+
|
|
63
|
+
<text
|
|
64
|
+
style={{
|
|
65
|
+
fontSize: '10px',
|
|
66
|
+
color: TEXT_PRIMARY,
|
|
67
|
+
fontFamily: 'monospace',
|
|
68
|
+
minWidth: '40px',
|
|
69
|
+
textAlign: 'center',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{`${value}${unit}`}
|
|
73
|
+
</text>
|
|
74
|
+
|
|
75
|
+
<view bindtap={increase} style={buttonStyle(canIncrease)}>
|
|
76
|
+
<text style={{ fontSize: '12px', color: canIncrease ? TEXT_PRIMARY : TEXT_TERTIARY, fontFamily: 'monospace', lineHeight: '14px' }}>
|
|
77
|
+
+
|
|
78
|
+
</text>
|
|
79
|
+
</view>
|
|
80
|
+
</view>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback } from '@lynx-js/react'
|
|
2
|
+
import { useDevPanelContext } from '../context'
|
|
3
|
+
import {
|
|
4
|
+
TRIGGER_SIZE,
|
|
5
|
+
TRIGGER_RADIUS,
|
|
6
|
+
TRIGGER_BG_OPEN,
|
|
7
|
+
TRIGGER_BG_CLOSED,
|
|
8
|
+
TEXT_PRIMARY,
|
|
9
|
+
SAFE_AREA_TOP,
|
|
10
|
+
} from '../constants'
|
|
11
|
+
|
|
12
|
+
export function DevPanelTrigger(): React.ReactElement {
|
|
13
|
+
const { open, setOpen } = useDevPanelContext()
|
|
14
|
+
|
|
15
|
+
const toggle = useCallback(() => {
|
|
16
|
+
setOpen(!open)
|
|
17
|
+
}, [open, setOpen])
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<view
|
|
21
|
+
bindtap={toggle}
|
|
22
|
+
style={{
|
|
23
|
+
position: 'absolute',
|
|
24
|
+
top: SAFE_AREA_TOP,
|
|
25
|
+
right: '8px',
|
|
26
|
+
width: TRIGGER_SIZE,
|
|
27
|
+
height: TRIGGER_SIZE,
|
|
28
|
+
borderRadius: TRIGGER_RADIUS,
|
|
29
|
+
backgroundColor: open ? TRIGGER_BG_OPEN : TRIGGER_BG_CLOSED,
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
justifyContent: 'center',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<text
|
|
35
|
+
style={{
|
|
36
|
+
fontSize: '12px',
|
|
37
|
+
color: TEXT_PRIMARY,
|
|
38
|
+
fontFamily: 'monospace',
|
|
39
|
+
lineHeight: '14px',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{open ? '\u00D7' : '\u2630'}
|
|
43
|
+
</text>
|
|
44
|
+
</view>
|
|
45
|
+
)
|
|
46
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Panel styling - minimalist devtool aesthetic
|
|
2
|
+
export const PANEL_BG = 'rgba(0,0,0,0.88)'
|
|
3
|
+
export const PANEL_RADIUS = '6px'
|
|
4
|
+
export const PANEL_PADDING = '8px'
|
|
5
|
+
|
|
6
|
+
// Safe area - iOS status bar height
|
|
7
|
+
export const SAFE_AREA_TOP = '48px'
|
|
8
|
+
|
|
9
|
+
// Trigger button - smaller, more subtle
|
|
10
|
+
export const TRIGGER_SIZE = '24px'
|
|
11
|
+
export const TRIGGER_RADIUS = '4px'
|
|
12
|
+
export const TRIGGER_BG_OPEN = 'rgba(0,0,0,0.85)'
|
|
13
|
+
export const TRIGGER_BG_CLOSED = 'rgba(0,0,0,0.18)' // More subtle when not active
|
|
14
|
+
|
|
15
|
+
// Panel content position (below trigger)
|
|
16
|
+
export const PANEL_TOP = '80px' // SAFE_AREA_TOP + TRIGGER_SIZE + 8px gap
|
|
17
|
+
|
|
18
|
+
// Text colors - muted, devtool-like
|
|
19
|
+
export const TEXT_PRIMARY = 'rgba(255,255,255,0.92)'
|
|
20
|
+
export const TEXT_SECONDARY = 'rgba(255,255,255,0.55)'
|
|
21
|
+
export const TEXT_TERTIARY = 'rgba(255,255,255,0.38)'
|
|
22
|
+
|
|
23
|
+
// FPS color coding - subtle, not harsh
|
|
24
|
+
export const FPS_GREEN = 'rgba(129,199,132,0.9)'
|
|
25
|
+
export const FPS_ORANGE = 'rgba(255,183,77,0.9)'
|
|
26
|
+
export const FPS_RED = 'rgba(229,115,115,0.9)'
|
|
27
|
+
|
|
28
|
+
// Stepper - compact
|
|
29
|
+
export const STEPPER_BG = 'rgba(255,255,255,0.08)'
|
|
30
|
+
export const STEPPER_SIZE = '20px'
|
|
31
|
+
export const STEPPER_RADIUS = '3px'
|
|
32
|
+
|
|
33
|
+
// Button
|
|
34
|
+
export const BUTTON_BG = 'rgba(255,255,255,0.06)'
|
|
35
|
+
export const BUTTON_ACTIVE = 'rgba(255,255,255,0.15)'
|
|
36
|
+
|
|
37
|
+
// Helper: get FPS color based on value
|
|
38
|
+
export function getFpsColor(fps: number): string {
|
|
39
|
+
return fps >= 50 ? FPS_GREEN : fps >= 30 ? FPS_ORANGE : FPS_RED
|
|
40
|
+
}
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createContext, useContext, useState } from '@lynx-js/react'
|
|
2
|
+
import type { DevPanelContextValue } from './types'
|
|
3
|
+
|
|
4
|
+
const DevPanelContext = createContext<DevPanelContextValue | null>(null)
|
|
5
|
+
|
|
6
|
+
export function useDevPanelContext(): DevPanelContextValue {
|
|
7
|
+
const ctx = useContext(DevPanelContext)
|
|
8
|
+
if (!ctx) {
|
|
9
|
+
throw new Error('DevPanel components must be used within DevPanel.Root')
|
|
10
|
+
}
|
|
11
|
+
return ctx
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DevPanelProviderProps {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
defaultOpen?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DevPanelProvider({
|
|
20
|
+
children,
|
|
21
|
+
defaultOpen = false,
|
|
22
|
+
}: DevPanelProviderProps): React.ReactElement {
|
|
23
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<DevPanelContext.Provider value={{ open, setOpen }}>
|
|
27
|
+
{children}
|
|
28
|
+
</DevPanelContext.Provider>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useRef, useMainThreadRef, runOnBackground, useState } from '@lynx-js/react'
|
|
2
|
+
import type { MainThreadRef } from '@lynx-js/react'
|
|
3
|
+
import type { MainThread } from '@lynx-js/types'
|
|
4
|
+
|
|
5
|
+
export interface UseDevPanelFPSReturn {
|
|
6
|
+
mtsFpsTick: () => void
|
|
7
|
+
btsFpsTick: () => void
|
|
8
|
+
mtsFpsDisplay: number
|
|
9
|
+
btsFpsDisplay: number
|
|
10
|
+
mtsFpsTextRef: MainThreadRef<MainThread.Element | null>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useDevPanelFPS(mtsDirectUpdate = false): UseDevPanelFPSReturn {
|
|
14
|
+
// Display state (for React render)
|
|
15
|
+
const [mtsFpsDisplay, setMtsFpsDisplay] = useState(0)
|
|
16
|
+
const [btsFpsDisplay, setBtsFpsDisplay] = useState(0)
|
|
17
|
+
|
|
18
|
+
// MTS refs for frame counting
|
|
19
|
+
const mtsFrameCountRef = useMainThreadRef(0)
|
|
20
|
+
const mtsLastTimeRef = useMainThreadRef(0)
|
|
21
|
+
|
|
22
|
+
// BTS refs for frame counting
|
|
23
|
+
const btsFrameCountRef = useRef(0)
|
|
24
|
+
const btsLastTimeRef = useRef(0)
|
|
25
|
+
|
|
26
|
+
// MTS direct update ref (for mtsDirectUpdate mode)
|
|
27
|
+
// Must use useMainThreadRef since it's passed to main-thread:ref
|
|
28
|
+
const mtsFpsTextRef = useMainThreadRef<MainThread.Element | null>(null)
|
|
29
|
+
|
|
30
|
+
// MTS tick function - must have 'main thread' directive first
|
|
31
|
+
function mtsFpsTick(): void {
|
|
32
|
+
'main thread'
|
|
33
|
+
mtsFrameCountRef.current++
|
|
34
|
+
const now = Date.now()
|
|
35
|
+
if (mtsLastTimeRef.current === 0) {
|
|
36
|
+
mtsLastTimeRef.current = now
|
|
37
|
+
}
|
|
38
|
+
const elapsed = now - mtsLastTimeRef.current
|
|
39
|
+
|
|
40
|
+
if (elapsed >= 500) {
|
|
41
|
+
const fps = Math.round((mtsFrameCountRef.current / elapsed) * 1000)
|
|
42
|
+
mtsFrameCountRef.current = 0
|
|
43
|
+
mtsLastTimeRef.current = now
|
|
44
|
+
|
|
45
|
+
// Direct update mode: update text element directly on MTS
|
|
46
|
+
if (mtsDirectUpdate && mtsFpsTextRef.current) {
|
|
47
|
+
mtsFpsTextRef.current.setAttribute('text', `${fps}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Push to BTS for state (used by color coding)
|
|
51
|
+
runOnBackground(setMtsFpsDisplay)(fps)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// BTS tick function - plain function
|
|
56
|
+
function btsFpsTick(): void {
|
|
57
|
+
btsFrameCountRef.current++
|
|
58
|
+
const now = Date.now()
|
|
59
|
+
if (btsLastTimeRef.current === 0) {
|
|
60
|
+
btsLastTimeRef.current = now
|
|
61
|
+
}
|
|
62
|
+
const elapsed = now - btsLastTimeRef.current
|
|
63
|
+
|
|
64
|
+
if (elapsed >= 500) {
|
|
65
|
+
const fps = Math.round((btsFrameCountRef.current / elapsed) * 1000)
|
|
66
|
+
btsFrameCountRef.current = 0
|
|
67
|
+
btsLastTimeRef.current = now
|
|
68
|
+
setBtsFpsDisplay(fps)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
mtsFpsTick,
|
|
74
|
+
btsFpsTick,
|
|
75
|
+
mtsFpsDisplay,
|
|
76
|
+
btsFpsDisplay,
|
|
77
|
+
mtsFpsTextRef,
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Context
|
|
2
|
+
export { DevPanelProvider, useDevPanelContext } from './context'
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type {
|
|
6
|
+
DevPanelRootProps,
|
|
7
|
+
DevPanelContentProps,
|
|
8
|
+
DevPanelStatProps,
|
|
9
|
+
DevPanelStepperProps,
|
|
10
|
+
DevPanelButtonProps,
|
|
11
|
+
DevPanelStatsProps,
|
|
12
|
+
DevPanelActionsProps,
|
|
13
|
+
DevPanelContextValue,
|
|
14
|
+
} from './types'
|
|
15
|
+
|
|
16
|
+
// Hook
|
|
17
|
+
export { useDevPanelFPS } from './hooks/useDevPanelFPS'
|
|
18
|
+
export type { UseDevPanelFPSReturn } from './hooks/useDevPanelFPS'
|
|
19
|
+
|
|
20
|
+
// FPS Component (requires props from hook)
|
|
21
|
+
export { DevPanelFPS } from './components/DevPanelFPS'
|
|
22
|
+
export type { DevPanelFPSProps } from './components/DevPanelFPS'
|
|
23
|
+
|
|
24
|
+
// Constants (for advanced customization)
|
|
25
|
+
export {
|
|
26
|
+
PANEL_BG,
|
|
27
|
+
PANEL_RADIUS,
|
|
28
|
+
PANEL_PADDING,
|
|
29
|
+
PANEL_TOP,
|
|
30
|
+
SAFE_AREA_TOP,
|
|
31
|
+
TRIGGER_SIZE,
|
|
32
|
+
TRIGGER_RADIUS,
|
|
33
|
+
TRIGGER_BG_OPEN,
|
|
34
|
+
TRIGGER_BG_CLOSED,
|
|
35
|
+
TEXT_PRIMARY,
|
|
36
|
+
TEXT_SECONDARY,
|
|
37
|
+
TEXT_TERTIARY,
|
|
38
|
+
FPS_GREEN,
|
|
39
|
+
FPS_ORANGE,
|
|
40
|
+
FPS_RED,
|
|
41
|
+
STEPPER_BG,
|
|
42
|
+
STEPPER_SIZE,
|
|
43
|
+
STEPPER_RADIUS,
|
|
44
|
+
BUTTON_BG,
|
|
45
|
+
BUTTON_ACTIVE,
|
|
46
|
+
getFpsColor,
|
|
47
|
+
} from './constants'
|
|
48
|
+
|
|
49
|
+
// Components
|
|
50
|
+
import { DevPanelRoot } from './components/DevPanelRoot'
|
|
51
|
+
import { DevPanelTrigger } from './components/DevPanelTrigger'
|
|
52
|
+
import { DevPanelContent } from './components/DevPanelContent'
|
|
53
|
+
import { DevPanelFPS } from './components/DevPanelFPS'
|
|
54
|
+
import { DevPanelStats } from './components/DevPanelStats'
|
|
55
|
+
import { DevPanelStat } from './components/DevPanelStat'
|
|
56
|
+
import { DevPanelStepper } from './components/DevPanelStepper'
|
|
57
|
+
import { DevPanelActions } from './components/DevPanelActions'
|
|
58
|
+
import { DevPanelButton } from './components/DevPanelButton'
|
|
59
|
+
|
|
60
|
+
// Namespace assembly (Radix-style compound component API)
|
|
61
|
+
export const DevPanel = {
|
|
62
|
+
Root: DevPanelRoot,
|
|
63
|
+
Trigger: DevPanelTrigger,
|
|
64
|
+
Content: DevPanelContent,
|
|
65
|
+
FPS: DevPanelFPS,
|
|
66
|
+
Stats: DevPanelStats,
|
|
67
|
+
Stat: DevPanelStat,
|
|
68
|
+
Stepper: DevPanelStepper,
|
|
69
|
+
Actions: DevPanelActions,
|
|
70
|
+
Button: DevPanelButton,
|
|
71
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { MainThread } from '@lynx-js/types'
|
|
2
|
+
|
|
3
|
+
export interface DevPanelRootProps {
|
|
4
|
+
children: React.ReactNode
|
|
5
|
+
defaultOpen?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DevPanelContentProps {
|
|
9
|
+
title: string
|
|
10
|
+
description?: string
|
|
11
|
+
children?: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DevPanelStatProps {
|
|
15
|
+
label: string
|
|
16
|
+
value: string | number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DevPanelStepperProps {
|
|
20
|
+
label: string
|
|
21
|
+
value: number
|
|
22
|
+
min?: number
|
|
23
|
+
max?: number
|
|
24
|
+
step?: number
|
|
25
|
+
unit?: string
|
|
26
|
+
onChange: (value: number) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DevPanelButtonProps {
|
|
30
|
+
label: string
|
|
31
|
+
active?: boolean
|
|
32
|
+
onPress: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DevPanelStatsProps {
|
|
36
|
+
children: React.ReactNode
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DevPanelActionsProps {
|
|
40
|
+
children: React.ReactNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Context types
|
|
44
|
+
export interface DevPanelContextValue {
|
|
45
|
+
open: boolean
|
|
46
|
+
setOpen: (open: boolean) => void
|
|
47
|
+
}
|