From c0856f8024b1a28a48ad37e0a8c44fb3b3fc94f8 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Mon, 16 Dec 2024 05:44:35 -0800 Subject: [PATCH] bump --- packages/scan/src/core/index.ts | 13 +- .../core/web/assets/css/styles.tailwind.css | 32 +- .../core/web/components/widget/FpsMeter.tsx | 41 + .../core/web/components/widget/toolbar.tsx | 95 +-- packages/scan/src/core/web/toolbar.tsx | 709 +----------------- 5 files changed, 128 insertions(+), 762 deletions(-) create mode 100644 packages/scan/src/core/web/components/widget/FpsMeter.tsx diff --git a/packages/scan/src/core/index.ts b/packages/scan/src/core/index.ts index 04a1abae..05cb7cda 100644 --- a/packages/scan/src/core/index.ts +++ b/packages/scan/src/core/index.ts @@ -31,6 +31,8 @@ import { type getSession } from './monitor/utils'; // @ts-expect-error CSS import import styles from './web/assets/css/styles.css'; +let toolbarContainer: HTMLElement | null = null; + export interface Options { /** * Enable/disable scanning @@ -218,10 +220,19 @@ export const setOptions = (options: Options) => { instrumentation.isPaused.value = options.enabled === false; } + const previousOptions = ReactScanInternals.options.value; + ReactScanInternals.options.value = { ...ReactScanInternals.options.value, ...options, }; + + if (previousOptions.showToolbar && !options.showToolbar) { + if (toolbarContainer) { + toolbarContainer.remove(); + toolbarContainer = null; + } + } }; export const getOptions = () => ReactScanInternals.options; @@ -409,7 +420,7 @@ export const start = () => { ReactScanInternals.instrumentation = instrumentation; if (options.showToolbar) { - createToolbar(shadow); + toolbarContainer = createToolbar(shadow); } // Add this right after creating the container diff --git a/packages/scan/src/core/web/assets/css/styles.tailwind.css b/packages/scan/src/core/web/assets/css/styles.tailwind.css index 08585ff7..74aef75d 100644 --- a/packages/scan/src/core/web/assets/css/styles.tailwind.css +++ b/packages/scan/src/core/web/assets/css/styles.tailwind.css @@ -41,7 +41,6 @@ button { } } - /* Default scrollbar styles for all elements in shadow DOM */ :host *::-webkit-scrollbar { width: 6px; @@ -114,10 +113,12 @@ button { inset: 0; transform: translateX(-100%); animation: shimmer 2s infinite; - background: linear-gradient(to right, - transparent, - rgba(142, 97, 227, 0.3), - transparent); + background: linear-gradient( + to right, + transparent, + rgba(142, 97, 227, 0.3), + transparent + ); } @keyframes shimmer { @@ -126,12 +127,12 @@ button { } } - #react-scan-toolbar { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - z-index: 999999999; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; + z-index: 2147483651; text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; + -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; backface-visibility: hidden; @@ -197,7 +198,6 @@ button { @apply truncate; } - .react-scan-section { @apply flex flex-col py-1; @apply py-2 px-4; @@ -226,15 +226,15 @@ button { } .react-scan-string { - color: #9ECBFF; + color: #9ecbff; } .react-scan-number { - color: #79C7FF; + color: #79c7ff; } .react-scan-boolean { - color: #56B6C2; + color: #56b6c2; } .react-scan-input { @@ -272,10 +272,9 @@ button { @apply origin-center; @apply transition-transform duration-150; } - } -.react-scan-expanded>.react-scan-arrow:before { +.react-scan-expanded > .react-scan-arrow:before { transform: rotate(90deg); } @@ -337,7 +336,7 @@ button { } #react-scan-toolbar button:focus-visible { - outline: 2px solid #0070F3; + outline: 2px solid #0070f3; outline-offset: -2px; } @@ -362,7 +361,6 @@ button { #react-scan-toolbar::-webkit-scrollbar { width: 4px; height: 4px; - } #react-scan-toolbar::-webkit-scrollbar-track { diff --git a/packages/scan/src/core/web/components/widget/FpsMeter.tsx b/packages/scan/src/core/web/components/widget/FpsMeter.tsx new file mode 100644 index 00000000..69a46f64 --- /dev/null +++ b/packages/scan/src/core/web/components/widget/FpsMeter.tsx @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'preact/hooks'; +import { getFPS } from '../../../instrumentation'; +import { cn } from '@web-utils/helpers'; + +export const FpsMeter = () => { + const [fps, setFps] = useState(getFPS()); + + useEffect(() => { + const interval = setInterval(() => { + setFps(getFPS()); + }, 100); + + return () => clearInterval(interval); + }, []); + + let textColor = 'text-white'; + let bgColor = 'bg-neutral-700'; + + if (fps < 10) { + textColor = 'text-white'; + bgColor = 'bg-red-500'; + } else if (fps < 30) { + textColor = 'text-black'; + bgColor = 'bg-yellow-300'; + } + + return ( + + {fps} FPS + + ); +}; + +export default FpsMeter; diff --git a/packages/scan/src/core/web/components/widget/toolbar.tsx b/packages/scan/src/core/web/components/widget/toolbar.tsx index 09a75d3f..e5123563 100644 --- a/packages/scan/src/core/web/components/widget/toolbar.tsx +++ b/packages/scan/src/core/web/components/widget/toolbar.tsx @@ -1,9 +1,11 @@ -import { useCallback, useEffect, useMemo } from "preact/hooks"; -import { cn, readLocalStorage, saveLocalStorage } from "@web-utils/helpers"; -import { ReactScanInternals, setOptions, Store } from "../../../.."; -import { INSPECT_TOGGLE_ID } from "../../inspect-element/inspect-state-machine"; -import { getNearestFiberFromElement } from "../../inspect-element/utils"; -import { Icon } from "../icon"; +import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; +import { cn, readLocalStorage, saveLocalStorage } from '@web-utils/helpers'; +import { ReactScanInternals, setOptions, Store } from '../../../..'; +import { INSPECT_TOGGLE_ID } from '../../inspect-element/inspect-state-machine'; +import { getNearestFiberFromElement } from '../../inspect-element/utils'; +import { Icon } from '../icon'; +import { getFPS } from '../../../instrumentation'; +import { FpsMeter } from './FpsMeter'; interface ToolbarProps { refPropContainer: preact.RefObject; @@ -175,66 +177,73 @@ export const Toolbar = ({ refPropContainer }: ToolbarProps) => { - { - isInspectFocused && ( -
+ - -
- ) - } - + + + + )} +
- react-scan - +
react-scan
+ +
); }; diff --git a/packages/scan/src/core/web/toolbar.tsx b/packages/scan/src/core/web/toolbar.tsx index 0d48d36d..3a3c0b98 100644 --- a/packages/scan/src/core/web/toolbar.tsx +++ b/packages/scan/src/core/web/toolbar.tsx @@ -1,713 +1,16 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; -import { - // signal, - useSignalEffect, - type Signal -} from '@preact/signals'; import { render } from 'preact'; -import { cn, throttle } from '@web-utils/helpers'; -import { - // ReactScanInternals, - setOptions, - Store -} from '../../index'; -import { - INSPECT_TOGGLE_ID, - type States, -} from './inspect-element/inspect-state-machine'; -import { getNearestFiberFromElement } from './inspect-element/utils'; -import { Icon } from './components/icon'; import { Widget } from './components/widget'; -import { Header } from './components/widget/header'; - -// const isSoundOnSignal = signal(false); - -// Sizing and positioning signals -const CORNER_KEY = 'react-scan-toolbar-corner'; -type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; -const DEFAULT_CORNER: Corner = 'top-left'; // Default is top-left - -// Function to save corner position -const saveCornerPosition = (corner: Corner) => { - localStorage.setItem(CORNER_KEY, corner); -}; - -// // Update initial position signals -// const toolbarX = signal( -// parseInt( -// typeof window !== 'undefined' -// ? localStorage.getItem('react-scan-toolbar-x') ?? '0' -// : '0', -// ), -// ); - -// const toolbarY = signal( -// parseInt( -// typeof window !== 'undefined' -// ? localStorage.getItem('react-scan-toolbar-y') ?? '0' -// : '0', -// ), -// ); - -// const isDragging = signal(false); -// const isResizing = signal(false); - -// Separate references for resizing and dragging -const initialWidthRef = { current: 0 }; // Used only for resizing -const initialMouseXRef = { current: 0 }; // Used only for resizing - -// Drag references -const dragInitialXOffsetRef = { current: 0 }; -const dragInitialYOffsetRef = { current: 0 }; const defaultWidth = 360; -const MIN_WIDTH = 360; -const MAX_WIDTH_RATIO = 0.5; -const EDGE_PADDING = 15; -const ANIMATION_DURATION = 300; // ms -const TRANSITION_TIMING = 'cubic-bezier(0.4, 0, 0.2, 1)'; - -const persistSizeToLocalStorage = throttle((width: number) => { - localStorage.setItem('react-scan-toolbar-width', String(width)); -}, 100); export const restoreSizeFromLocalStorage = (): number => { const width = localStorage.getItem('react-scan-toolbar-width'); return width ? parseInt(width, 10) : defaultWidth; }; -const persistPositionToLocalStorage = throttle((x: number, y: number) => { - localStorage.setItem('react-scan-toolbar-x', String(x)); - localStorage.setItem('react-scan-toolbar-y', String(y)); -}, 100); - -// Ensure width stays within bounds and handle edge cases -const getConstrainedWidth = (width: number, maxWidth?: number | null): number => { - // Base width with border adjustment - const adjustedWidth = width - 2; // 2px for borders - - // Calculate max allowed width based on window or parent - const maxAllowedWidth = (() => { - // If valid maxWidth is provided (from parent), use it with padding - if (typeof maxWidth === 'number' && maxWidth > 0) { - return maxWidth - (2 * EDGE_PADDING); - } - - // Fallback to window width with ratio and padding - return (window.innerWidth * MAX_WIDTH_RATIO) - (2 * EDGE_PADDING); - })(); - - // Ensure width is between MIN_WIDTH and maxAllowedWidth - return Math.max( - MIN_WIDTH, - Math.min(adjustedWidth, maxAllowedWidth) - ); -}; - -// Update props interface to make isPaused optional -interface ToolbarProps { - inspectState: Signal; - isPaused: Signal; - isSoundOn: Signal; - x: Signal; - y: Signal; - isDragging: Signal; - isResizing: Signal; -} - -// Update component to unwrap signals -export const Toolbar = ({ - inspectState, - isPaused, - isSoundOn, - x, - y, - isDragging, - isResizing, -}: ToolbarProps) => { - const refTimer = useRef(); - - const refToolbarContent = useRef(null); - const refToolbarContentInitialWidth = useRef(); - const refPropContainer = useRef(null); - const refResizeHandle = useRef(null); - const refToolbar = useRef(null); - - const [width, setWidth] = useState(restoreSizeFromLocalStorage); - - const focusActive = inspectState.value.kind === 'focused'; - const isInspectActive = inspectState.value.kind === 'inspecting'; - - useEffect(() => { - localStorage.setItem('react-scan-paused', String(!isPaused?.value)); - }, [isPaused?.value]); - - useEffect(() => { - if (refToolbarContent.current && refPropContainer.current) { - const maxWidth = getConstrainedWidth(width, refPropContainer.current.parentElement?.clientWidth); - refToolbarContent.current.style.maxHeight = focusActive ? '50vh' : `39px`; - refToolbarContent.current.style.maxWidth = `${maxWidth}px`; - refPropContainer.current.style.minWidth = focusActive ? `${maxWidth - 2}px` : '100%'; - } - }, [focusActive]); - - // Add a ref to track if initial position is set - const isInitialPositionSet = useRef(false); - - // Add a single useEffect for position handling - useEffect(() => { - if (!refToolbar.current || isInitialPositionSet.current) return; - - // Get saved corner - const savedCorner = localStorage.getItem(CORNER_KEY) as Corner; - const rect = refToolbar.current.getBoundingClientRect(); - - // Calculate position based on saved corner or default - let newX, newY; - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - switch (savedCorner || DEFAULT_CORNER) { - case 'top-left': - newX = EDGE_PADDING; - newY = EDGE_PADDING; - break; - case 'top-right': - newX = viewportWidth - rect.width - EDGE_PADDING; - newY = EDGE_PADDING; - break; - case 'bottom-left': - newX = EDGE_PADDING; - newY = viewportHeight - rect.height - EDGE_PADDING; - break; - case 'bottom-right': - newX = viewportWidth - rect.width - EDGE_PADDING; - newY = viewportHeight - rect.height - EDGE_PADDING; - break; - } - - // Set position - x.value = newX; - y.value = newY; - refToolbar.current.style.transform = `translate(${newX}px, ${newY}px)`; - persistPositionToLocalStorage(newX, newY); - - isInitialPositionSet.current = true; - - refToolbar.current?.classList.add('animate-fade-in', 'animate-duration-300', 'animate-delay-300'); - }, []); - - // Only save corner in ensureToolbarInBounds when actually dragging - const ensureToolbarInBounds = useCallback(() => { - if (!refToolbar.current) return; - - const toolbarRect = refToolbar.current.getBoundingClientRect(); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // Calculate distances to edges - const distanceToLeft = toolbarRect.left; - const distanceToRight = viewportWidth - toolbarRect.right; - const distanceToTop = toolbarRect.top; - const distanceToBottom = viewportHeight - toolbarRect.bottom; - - // Determine corner based on closest edges - let corner: Corner; - if (distanceToTop <= distanceToBottom) { - corner = distanceToLeft <= distanceToRight ? 'top-left' : 'top-right'; - } else { - corner = distanceToLeft <= distanceToRight ? 'bottom-left' : 'bottom-right'; - } - - saveCornerPosition(corner); - - // Calculate new position based on corner - let newX, newY; - switch (corner) { - case 'top-left': - newX = EDGE_PADDING; - newY = EDGE_PADDING; - break; - case 'top-right': - newX = viewportWidth - toolbarRect.width - EDGE_PADDING; - newY = EDGE_PADDING; - break; - case 'bottom-left': - newX = EDGE_PADDING; - newY = viewportHeight - toolbarRect.height - EDGE_PADDING; - break; - case 'bottom-right': - newX = viewportWidth - toolbarRect.width - EDGE_PADDING; - newY = viewportHeight - toolbarRect.height - EDGE_PADDING; - break; - } - - x.value = newX; - y.value = newY; - persistPositionToLocalStorage(newX, newY); - - // Smoother transition - refToolbar.current.style.transition = `transform ${ANIMATION_DURATION}ms ${TRANSITION_TIMING}`; - refToolbar.current.style.transform = `translate(${newX}px, ${newY}px)`; - refTimer.current = setTimeout(() => { - if (refToolbar.current) { - refToolbar.current.style.transition = ''; - } - }, ANIMATION_DURATION); - }, [isDragging.value]); - - useEffect(() => { - refToolbarContentInitialWidth.current = refToolbarContent.current?.offsetWidth ?? 0; - const handleViewportChange = throttle(() => { - if (!isDragging.value && !isResizing.value) { - ensureToolbarInBounds(); - } - }, 100); - - handleViewportChange(); - - window.addEventListener('resize', handleViewportChange); - window.addEventListener('scroll', handleViewportChange); - - return () => { - window.removeEventListener('resize', handleViewportChange); - window.removeEventListener('scroll', handleViewportChange); - }; - }, []); - - - // Mouse events for resizing - useEffect(() => { - const onMouseMove = (e: MouseEvent) => { - clearTimeout(refTimer.current); - if ( - isResizing.value - && refToolbarContent.current - && refToolbar.current - && refPropContainer.current - ) { - const w = initialWidthRef.current - (e.clientX - initialMouseXRef.current); - - // Calculate max width with padding consideration - const maxWidth = (window.innerWidth * MAX_WIDTH_RATIO) - (2 * EDGE_PADDING); - const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, w)); - - refToolbarContent.current.style.maxWidth = `${newWidth}px`; - refPropContainer.current.style.minWidth = focusActive ? `${newWidth - 2}px` : '100%'; - - - refTimer.current = setTimeout(() => { - setWidth(newWidth); - persistSizeToLocalStorage(newWidth); - }, 100); - } - }; - - const onMouseUp = () => { - if (isDragging.value) { - isDragging.value = false; - ensureToolbarInBounds(); - } - if (isResizing.value) { - isResizing.value = false; - } - }; - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - }, [isResizing.value]); - - const onToggleActive = () => { - isPaused.value = !isPaused.value; - }; - - // pivanov - useEffect(() => { - const currentState = Store.inspectState.value; - - if (currentState.kind === 'uninitialized') { - Store.inspectState.value = { - kind: 'inspect-off', - propContainer: refPropContainer.current!, - }; - } - }, []); - - const onToggleInspect = useCallback(() => { - const currentState = Store.inspectState.value; - switch (currentState.kind) { - case 'inspecting': - Store.inspectState.value = { - kind: 'inspect-off', - propContainer: currentState.propContainer, - }; - break; - case 'focused': - Store.inspectState.value = { - kind: 'inspect-off', - propContainer: currentState.propContainer, - }; - break; - case 'inspect-off': - Store.inspectState.value = { - kind: 'inspecting', - hoveredDomElement: null, - propContainer: refPropContainer.current!, - }; - break; - case 'uninitialized': - break; - } - }, [Store.inspectState.value]); - - const onSoundToggle = useCallback(() => { - isSoundOn.value = !isSoundOn.value; - setOptions({ playSound: isSoundOn.value }); - }, [isSoundOn.value]); - - const onNextFocus = useCallback(() => { - const currentState = Store.inspectState.value; - if (currentState.kind !== 'focused' || !currentState.focusedDomElement) - return; - - const focusedDomElement = currentState.focusedDomElement; - const allElements = Array.from(document.querySelectorAll('*')).filter( - (el): el is HTMLElement => el instanceof HTMLElement, - ); - const currentIndex = allElements.indexOf(focusedDomElement); - if (currentIndex === -1) return; - - let nextElement: HTMLElement | null = null; - let nextIndex = currentIndex + 1; - const prevFiber = getNearestFiberFromElement(focusedDomElement); - - while (nextIndex < allElements.length) { - const fiber = getNearestFiberFromElement(allElements[nextIndex]); - if (fiber && fiber !== prevFiber) { - nextElement = allElements[nextIndex]; - break; - } - nextIndex++; - } - - if (nextElement) { - Store.inspectState.value = { - kind: 'focused', - focusedDomElement: nextElement, - propContainer: currentState.propContainer, - }; - } - }, [Store.inspectState.value]); - - const onPreviousFocus = useCallback(() => { - const currentState = Store.inspectState.value; - if (currentState.kind !== 'focused' || !currentState.focusedDomElement) - return; - - const focusedDomElement = currentState.focusedDomElement; - const allElements = Array.from(document.querySelectorAll('*')).filter( - (el): el is HTMLElement => el instanceof HTMLElement, - ); - const currentIndex = allElements.indexOf(focusedDomElement); - if (currentIndex === -1) return; - - let prevElement: HTMLElement | null = null; - let prevIndex = currentIndex - 1; - const currentFiber = getNearestFiberFromElement(focusedDomElement); - - while (prevIndex >= 0) { - const fiber = getNearestFiberFromElement(allElements[prevIndex]); - if (fiber && fiber !== currentFiber) { - prevElement = allElements[prevIndex]; - break; - } - prevIndex--; - } - - if (prevElement) { - Store.inspectState.value = { - kind: 'focused', - focusedDomElement: prevElement, - propContainer: currentState.propContainer, - }; - } - }, [Store.inspectState.value]); - - - const { inspectIcon, inspectColor } = useMemo(() => { - let inspectIcon = null; - let inspectColor = '#999'; - - if (isInspectActive) { - inspectIcon = ; - inspectColor = 'rgba(142, 97, 227, 1)'; - } else if (focusActive) { - inspectIcon = ; - inspectColor = 'rgba(142, 97, 227, 1)'; - } else { - inspectIcon = ; - inspectColor = '#999'; - } - - return { inspectIcon, inspectColor }; - }, [isInspectActive, focusActive]); - - const onMouseDownToolbar = useCallback((e: MouseEvent) => { - e.preventDefault(); - - const target = e.target as HTMLElement; - if (target.closest('button') ?? target === refResizeHandle.current) { - return; - } - - isDragging.value = true; - dragInitialXOffsetRef.current = e.clientX - x.value; - dragInitialYOffsetRef.current = e.clientY - y.value; - - // Remove transition during drag for immediate response - if (refToolbar.current) { - refToolbar.current.style.transition = 'none'; - } - }, [x, y, refToolbar, refResizeHandle]); - - const onMouseDownResize = useCallback((e: MouseEvent) => { - e.preventDefault(); - isResizing.value = true; - initialWidthRef.current = refPropContainer.current!.offsetWidth; - initialMouseXRef.current = e.clientX; - }, []); - - useEffect(() => { - if (refToolbar.current) { - refToolbar.current.style.transform = `translate(${x.value}px, ${y.value}px)`; - } - }, []); - - // Update toolbar position during drag - useEffect(() => { - const onMouseMove = (e: MouseEvent) => { - if (isDragging.value && refToolbar.current) { - const newX = e.clientX - dragInitialXOffsetRef.current; - const newY = e.clientY - dragInitialYOffsetRef.current; - x.value = newX; - y.value = newY; - refToolbar.current.style.transform = `translate(${newX}px, ${newY}px)`; - persistPositionToLocalStorage(newX, newY); - } - }; - const onMouseUp = () => { - if (isDragging.value) { - isDragging.value = false; - ensureToolbarInBounds(); - } - }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - }, []); - - useSignalEffect(() => { - if (refToolbar.current) { - refToolbar.current.style.transform = `translate(${x.value}px, ${y.value}px)`; - } - }); - - useEffect(() => { - if (refToolbar.current) { - const rect = refToolbar.current.getBoundingClientRect(); - x.value = rect.left; - y.value = rect.top; - } - }, []); - - useSignalEffect(() => { - if (Store.inspectState.value.kind === 'focused') { - ensureToolbarInBounds(); - } - }); - - useEffect(() => { - if (!refToolbar.current) return; - - const observer = new ResizeObserver(() => { - ensureToolbarInBounds(); - }); - - observer.observe(refToolbar.current); - - return () => { - observer.disconnect(); - }; - }, []); - - return ( -
-
-
- -
- {/* Inject props content here if needed */} -
- -
- - - - -
- { - focusActive && ( -
- - -
- ) - } - - react-scan - -
-
- -
-
-
- ); -}; - -export const createToolbar = (shadow: ShadowRoot) => { +export function createToolbar(shadow: ShadowRoot): HTMLElement | null { if (typeof window === 'undefined') { - return; + return null; } const ToolbarWrapper = () => ( @@ -725,5 +28,9 @@ export const createToolbar = (shadow: ShadowRoot) => { ); - render(, shadow); -}; + const root = document.createElement('div'); + shadow.appendChild(root); + render(, root); + + return root; +}