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 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
+ }
@@ -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
+ }
@@ -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
+ }