From d3fc41a4bbe0473563e6745d1458a6da0f252dc9 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Tue, 17 Dec 2024 11:25:48 +0200 Subject: [PATCH] fix: widget d&d, resizing, snapping and improve widget UI and overall UX --- .vscode/settings.json | 14 +- .../core/web/assets/css/styles.tailwind.css | 12 +- .../scan/src/core/web/assets/svgs/svgs.ts | 19 +- .../core/web/components/widget/fps-meter.tsx | 57 ++- .../src/core/web/components/widget/header.tsx | 19 - .../src/core/web/components/widget/helpers.ts | 277 +++++++---- .../src/core/web/components/widget/index.tsx | 361 ++++++++------ .../web/components/widget/resize-handle.tsx | 456 ++++++++++-------- .../core/web/components/widget/toolbar.tsx | 101 ++-- .../src/core/web/components/widget/types.ts | 1 - .../inspect-element/inspect-state-machine.ts | 5 +- packages/scan/src/core/web/state.ts | 3 - packages/scan/src/core/web/utils/helpers.ts | 4 + packages/scan/tailwind.config.mjs | 3 + packages/website/app/layout.tsx | 6 +- 15 files changed, 767 insertions(+), 571 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c27be254..9b74a920 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,17 @@ "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.preferences.importModuleSpecifier": "non-relative", - "typescript.preferences.includePackageJsonAutoImports": "on" + "typescript.preferences.includePackageJsonAutoImports": "on", + "files.associations": { + "*.css": "css" + }, + "editor.quickSuggestions": { + "strings": true + }, + "css.validate": false, + "tailwindCSS.validate": true, + "editor.colorDecorators": true, + "[css]": { + "editor.formatOnSave": false, + } } 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 74aef75d..2bf0c585 100644 --- a/packages/scan/src/core/web/assets/css/styles.tailwind.css +++ b/packages/scan/src/core/web/assets/css/styles.tailwind.css @@ -147,13 +147,11 @@ button { } .react-scan-header { - padding: 8px 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - gap: 8px; - align-items: center; - justify-content: space-between; - background: #000; + @apply flex items-center justify-between gap-x-4; + @apply rounded-t-lg; + @apply overflow-hidden; + @apply py-2 px-4; + @apply border-b-1 border-white/10; } .react-scan-header-left { diff --git a/packages/scan/src/core/web/assets/svgs/svgs.ts b/packages/scan/src/core/web/assets/svgs/svgs.ts index a927b7c1..6f4a2603 100644 --- a/packages/scan/src/core/web/assets/svgs/svgs.ts +++ b/packages/scan/src/core/web/assets/svgs/svgs.ts @@ -74,22 +74,13 @@ export const ICONS = ` - - - - - - - + + + - - - - - - - + + `; diff --git a/packages/scan/src/core/web/components/widget/fps-meter.tsx b/packages/scan/src/core/web/components/widget/fps-meter.tsx index 93d96bec..49d31e40 100644 --- a/packages/scan/src/core/web/components/widget/fps-meter.tsx +++ b/packages/scan/src/core/web/components/widget/fps-meter.tsx @@ -1,39 +1,52 @@ -import { useState, useEffect } from 'preact/hooks'; -import { cn } from '@web-utils/helpers'; +import { useEffect, useRef } from 'preact/hooks'; +import { cn, toggleMultipleClasses } from '@web-utils/helpers'; import { getFPS } from '../../../instrumentation'; export const FpsMeter = () => { - const [fps, setFps] = useState(getFPS()); + const refContainer = useRef(null); useEffect(() => { - const interval = setInterval(() => { - setFps(getFPS()); - }, 100); + let rafId: number; + let lastUpdate = performance.now(); + const UPDATE_INTERVAL = 100; - return () => clearInterval(interval); - }, []); + const updateFPS = () => { + const now = performance.now(); + if (now - lastUpdate >= UPDATE_INTERVAL) { + if (!refContainer.current) return; + const fps = getFPS(); + refContainer.current.dataset.text = fps.toString(); + if (fps < 10) { + toggleMultipleClasses(refContainer.current, 'text-white', 'bg-red-500', 'text-black', 'bg-yellow-300'); + } else if (fps < 30) { + toggleMultipleClasses(refContainer.current, 'text-white', 'bg-red-500', 'text-black', 'bg-yellow-300'); + } + + lastUpdate = now; + } + rafId = requestAnimationFrame(updateFPS); + }; - let textColor = 'text-white'; - let bgColor = 'bg-neutral-700'; + rafId = requestAnimationFrame(updateFPS); + return () => cancelAnimationFrame(rafId); + }, []); - if (fps < 10) { - textColor = 'text-white'; - bgColor = 'bg-red-500'; - } else if (fps < 30) { - textColor = 'text-black'; - bgColor = 'bg-yellow-300'; - } return ( - {fps} FPS + FPS ); }; diff --git a/packages/scan/src/core/web/components/widget/header.tsx b/packages/scan/src/core/web/components/widget/header.tsx index 3b1d9541..7bdb549d 100644 --- a/packages/scan/src/core/web/components/widget/header.tsx +++ b/packages/scan/src/core/web/components/widget/header.tsx @@ -51,24 +51,8 @@ export const Header = () => { const unsubscribe = Store.lastReportTime.subscribe(updateMetrics); - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && inspectState.kind === 'focused') { - if (Store.inspectState.value.propContainer) { - Store.inspectState.value.propContainer.innerHTML = ''; - Store.inspectState.value = { - kind: 'inspecting', - hoveredDomElement: inspectState.focusedDomElement, - propContainer: Store.inspectState.value.propContainer, - }; - } - } - }; - - window.addEventListener('keydown', handleKeyDown, { capture: true }); - return () => { unsubscribe(); - window.removeEventListener('keydown', handleKeyDown, { capture: true }); }; }, [inspectState]); @@ -115,9 +99,6 @@ export const Header = () => { "min-h-9", 'whitespace-nowrap', "overflow-hidden", - { - 'hidden': inspectState.kind !== 'focused' - } )} >
diff --git a/packages/scan/src/core/web/components/widget/helpers.ts b/packages/scan/src/core/web/components/widget/helpers.ts index 07acc703..81d72291 100644 --- a/packages/scan/src/core/web/components/widget/helpers.ts +++ b/packages/scan/src/core/web/components/widget/helpers.ts @@ -1,52 +1,57 @@ import { SAFE_AREA, MIN_SIZE } from '../../constants'; import { type Corner, type Position, type ResizeHandleProps, type Size } from './types'; -interface WindowDimensionsCache { - width: number; - height: number; - dimensions: { +export const getWindowDimensions = (() => { + let cache: { + width: number; + height: number; maxWidth: number; maxHeight: number; rightEdge: (width: number) => number; bottomEdge: (height: number) => number; isFullWidth: (width: number) => boolean; isFullHeight: (height: number) => boolean; - } | null; -} - -const windowDimensionsCache: WindowDimensionsCache = { - width: 0, - height: 0, - dimensions: null -}; - -export const getWindowDimensions = () => { - const maxWidth = window.innerWidth - (SAFE_AREA * 2); - const maxHeight = window.innerHeight - (SAFE_AREA * 2); - - if ( - windowDimensionsCache.width === window.innerWidth && - windowDimensionsCache.height === window.innerHeight && - windowDimensionsCache.dimensions - ) { - return windowDimensionsCache.dimensions; - } - - const dimensions = { - maxWidth, - maxHeight, - rightEdge: (width: number) => window.innerWidth - width - SAFE_AREA, - bottomEdge: (height: number) => window.innerHeight - height - SAFE_AREA, - isFullWidth: (width: number) => width >= maxWidth, - isFullHeight: (height: number) => height >= maxHeight + } | null = null; + + return () => { + const currentWidth = window.innerWidth; + const currentHeight = window.innerHeight; + + if (cache && cache.width === currentWidth && cache.height === currentHeight) { + return { + maxWidth: cache.maxWidth, + maxHeight: cache.maxHeight, + rightEdge: cache.rightEdge, + bottomEdge: cache.bottomEdge, + isFullWidth: cache.isFullWidth, + isFullHeight: cache.isFullHeight + }; + } + + const maxWidth = currentWidth - (SAFE_AREA * 2); + const maxHeight = currentHeight - (SAFE_AREA * 2); + + cache = { + width: currentWidth, + height: currentHeight, + maxWidth, + maxHeight, + rightEdge: (width: number) => currentWidth - width - SAFE_AREA, + bottomEdge: (height: number) => currentHeight - height - SAFE_AREA, + isFullWidth: (width: number) => width >= maxWidth, + isFullHeight: (height: number) => height >= maxHeight + }; + + return { + maxWidth: cache.maxWidth, + maxHeight: cache.maxHeight, + rightEdge: cache.rightEdge, + bottomEdge: cache.bottomEdge, + isFullWidth: cache.isFullWidth, + isFullHeight: cache.isFullHeight + }; }; - - windowDimensionsCache.width = window.innerWidth; - windowDimensionsCache.height = window.innerHeight; - windowDimensionsCache.dimensions = dimensions; - - return dimensions; -}; +})(); export const getOppositeCorner = ( position: ResizeHandleProps['position'], @@ -85,60 +90,87 @@ export const getOppositeCorner = ( }; export const calculatePosition = (corner: Corner, width: number, height: number): Position => { - const { rightEdge, bottomEdge } = getWindowDimensions(); + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + // Calculate base positions switch (corner) { case 'top-right': - return { x: rightEdge(width), y: SAFE_AREA }; + return { + x: windowWidth - width - SAFE_AREA, + y: SAFE_AREA + }; case 'bottom-right': - return { x: rightEdge(width), y: bottomEdge(height) }; + return { + x: windowWidth - width - SAFE_AREA, + y: windowHeight - height - SAFE_AREA + }; case 'bottom-left': - return { x: SAFE_AREA, y: bottomEdge(height) }; + return { + x: SAFE_AREA, + y: windowHeight - height - SAFE_AREA + }; case 'top-left': default: - return { x: SAFE_AREA, y: SAFE_AREA }; + return { + x: SAFE_AREA, + y: SAFE_AREA + }; } }; -export const getPositionClasses = (position: ResizeHandleProps['position']) => { - return { - 'top-0 left-0 w-full -translate-y-1/2': position === 'top', - 'bottom-0 left-0 w-full translate-y-1/2': position === 'bottom', - 'left-0 top-0 h-full -translate-x-1/2': position === 'left', - 'right-0 top-0 h-full translate-x-1/2': position === 'right', - - 'top-0 left-0 -translate-x-1/2 -translate-y-1/2': position === 'top-left', - 'top-0 right-0 translate-x-1/2 -translate-y-1/2': position === 'top-right', - 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2': position === 'bottom-left', - 'bottom-0 right-0 translate-x-1/2 translate-y-1/2': position === 'bottom-right', - }; +export const getPositionClasses = (position: ResizeHandleProps['position']): string => { + switch (position) { + case 'top': return 'top-0 left-0 right-0 -translate-y-3/4'; + case 'bottom': return 'right-0 bottom-0 left-0 translate-y-3/4'; + case 'left': return 'top-0 bottom-0 left-0 -translate-x-3/4'; + case 'right': return 'top-0 right-0 bottom-0 translate-x-3/4'; + case 'top-left': return 'top-0 left-0 -translate-x-3/4 -translate-y-3/4'; + case 'top-right': return 'top-0 right-0 translate-x-3/4 -translate-y-3/4'; + case 'bottom-left': return 'bottom-0 left-0 -translate-x-3/4 translate-y-3/4'; + case 'bottom-right': return 'bottom-0 right-0 translate-x-3/4 translate-y-3/4'; + default: return ''; + } }; export const getInteractionClasses = ( position: ResizeHandleProps['position'], isLine: boolean, - isDiabled: boolean -) => { - return { - 'hover:opacity-100': !isDiabled, - 'pointer-events-none': isDiabled, - 'w-6 h-6': !isLine, - 'w-5 h-full': isLine && (position === 'left' || position === 'right'), - 'w-full h-5': isLine && (position === 'top' || position === 'bottom'), - 'rounded-tl': !isLine && position === 'top-left', - 'rounded-tr': !isLine && position === 'top-right', - 'rounded-bl': !isLine && position === 'bottom-left', - 'rounded-br': !isLine && position === 'bottom-right', - 'cursor-ew-resize': isLine && (position === 'left' || position === 'right'), - 'cursor-ns-resize': isLine && (position === 'top' || position === 'bottom'), - }; +): Array => { + // Common classes for both line and corner handles + const commonClasses = [ + 'transition-[transform,opacity]', + 'duration-300', + 'delay-500', + 'group-hover:delay-0', + 'group-active:delay-0', + ]; + + // Line handles + if (isLine) { + return [ + ...commonClasses, + // Size classes + position === 'left' || position === 'right' ? 'w-6' : 'w-full', + position === 'left' || position === 'right' ? 'h-full' : 'h-6', + // Cursor classes + position === 'left' || position === 'right' ? 'cursor-ew-resize' : 'cursor-ns-resize' + ]; + } + + // Corner handles only + return [ + ...commonClasses, + 'w-6', + 'h-6', + position === 'top-left' || position === 'bottom-right' ? 'cursor-nwse-resize' : 'cursor-nesw-resize', + `rounded-${position.split('-').join('')}` + ]; }; const positionMatchesCorner = (position: ResizeHandleProps['position'], corner: Corner): boolean => { - if (position === corner) return true; - const [vertical, horizontal] = corner.split('-'); - return position === vertical || position === horizontal; + return position !== vertical && position !== horizontal; }; export const getHandleVisibility = ( @@ -149,30 +181,34 @@ export const getHandleVisibility = ( isFullHeight: boolean ): boolean => { if (isFullWidth && isFullHeight) { - return false; + return true; } // Normal state if (!isFullWidth && !isFullHeight) { - return isLine - ? positionMatchesCorner(position, corner) - : position === corner || - (corner.includes(position.split('-')[0]) || corner.includes(position.split('-')[1])); + if (isLine) { + return positionMatchesCorner(position, corner); + } + return position === getOppositeCorner(corner, corner, true); } // Full width state if (isFullWidth) { - return isLine - ? position === 'top' && corner.startsWith('top') || - position === 'bottom' && corner.startsWith('bottom') - : corner.startsWith(position.split('-')[0]); + if (isLine) { + return position !== corner.split('-')[0]; + } + return !position.startsWith(corner.split('-')[0]); } // Full height state - return isLine - ? position === 'left' && corner.endsWith('left') || - position === 'right' && corner.endsWith('right') - : corner.endsWith(position.split('-')[1]); + if (isFullHeight) { + if (isLine) { + return position !== corner.split('-')[1]; + } + return !position.endsWith(corner.split('-')[1]); + } + + return false; }; @@ -181,10 +217,13 @@ export const calculateBoundedSize = ( delta: number, isWidth: boolean ): number => { - const min = isWidth ? MIN_SIZE.width : MIN_SIZE.height; - const max = isWidth ? getWindowDimensions().maxWidth : getWindowDimensions().maxHeight; + const min = isWidth ? MIN_SIZE.width : MIN_SIZE.height * 5; + const max = isWidth + ? getWindowDimensions().maxWidth + : getWindowDimensions().maxHeight; - return Math.min(Math.max(min, currentSize + delta), max); + const newSize = currentSize + delta; + return Math.min(Math.max(min, newSize), max); } export const calculateNewSizeAndPosition = ( @@ -204,22 +243,30 @@ export const calculateNewSizeAndPosition = ( // horizontal resize if (position.includes('right')) { - const proposedWidth = initialSize.width + deltaX; + // Check if we have enough space on the right + const availableWidth = window.innerWidth - initialPosition.x - SAFE_AREA; + const proposedWidth = Math.min(initialSize.width + deltaX, availableWidth); newWidth = Math.min(maxWidth, Math.max(MIN_SIZE.width, proposedWidth)); } if (position.includes('left')) { - const proposedWidth = initialSize.width - deltaX; + // Check if we have enough space on the left + const availableWidth = initialPosition.x + initialSize.width - SAFE_AREA; + const proposedWidth = Math.min(initialSize.width - deltaX, availableWidth); newWidth = Math.min(maxWidth, Math.max(MIN_SIZE.width, proposedWidth)); newX = initialPosition.x - (newWidth - initialSize.width); } // vertical resize if (position.includes('bottom')) { - const proposedHeight = initialSize.height + deltaY; + // Check if we have enough space at the bottom + const availableHeight = window.innerHeight - initialPosition.y - SAFE_AREA; + const proposedHeight = Math.min(initialSize.height + deltaY, availableHeight); newHeight = Math.min(maxHeight, Math.max(MIN_SIZE.height * 5, proposedHeight)); } if (position.includes('top')) { - const proposedHeight = initialSize.height - deltaY; + // Check if we have enough space at the top + const availableHeight = initialPosition.y + initialSize.height - SAFE_AREA; + const proposedHeight = Math.min(initialSize.height - deltaY, availableHeight); newHeight = Math.min(maxHeight, Math.max(MIN_SIZE.height * 5, proposedHeight)); newY = initialPosition.y - (newHeight - initialSize.height); } @@ -248,3 +295,45 @@ export const getClosestCorner = (position: Position): Corner => { return distance < distances[closest] ? corner as Corner : closest; }, 'top-left'); }; + +// Helper to determine best corner based on cursor position, widget size, and movement +export const getBestCorner = ( + mouseX: number, + mouseY: number, + initialMouseX?: number, + initialMouseY?: number, + threshold = 100 +): Corner => { + const deltaX = initialMouseX !== undefined ? mouseX - initialMouseX : 0; + const deltaY = initialMouseY !== undefined ? mouseY - initialMouseY : 0; + + const windowCenterX = window.innerWidth / 2; + const windowCenterY = window.innerHeight / 2; + + // Determine movement direction + const movingRight = deltaX > threshold; + const movingLeft = deltaX < -threshold; + const movingDown = deltaY > threshold; + const movingUp = deltaY < -threshold; + + // If horizontal movement + if (movingRight || movingLeft) { + const isBottom = mouseY > windowCenterY; + return movingRight + ? (isBottom ? 'bottom-right' : 'top-right') + : (isBottom ? 'bottom-left' : 'top-left'); + } + + // If vertical movement + if (movingDown || movingUp) { + const isRight = mouseX > windowCenterX; + return movingDown + ? (isRight ? 'bottom-right' : 'bottom-left') + : (isRight ? 'top-right' : 'top-left'); + } + + // If no significant movement, use quadrant-based position + return mouseX > windowCenterX + ? (mouseY > windowCenterY ? 'bottom-right' : 'top-right') + : (mouseY > windowCenterY ? 'bottom-left' : 'top-left'); +}; diff --git a/packages/scan/src/core/web/components/widget/index.tsx b/packages/scan/src/core/web/components/widget/index.tsx index 41f766a8..9998de65 100644 --- a/packages/scan/src/core/web/components/widget/index.tsx +++ b/packages/scan/src/core/web/components/widget/index.tsx @@ -1,95 +1,141 @@ import { type JSX } from 'preact'; -import { useRef, useCallback, useEffect, useMemo } from 'preact/hooks'; -import { cn, debounce, saveLocalStorage } from '@web-utils/helpers'; +import { useRef, useCallback, useEffect } from 'preact/hooks'; +import { cn, debounce, saveLocalStorage, toggleMultipleClasses } from '@web-utils/helpers'; import { getCompositeComponentFromElement } from '@web-inspect-element/utils'; import { Store } from 'src/core'; import { signalWidget, signalRefContainer, updateDimensions } from '../../state'; -import { SAFE_AREA, LOCALSTORAGE_KEY } from '../../constants'; +import { SAFE_AREA, LOCALSTORAGE_KEY, MIN_SIZE } from '../../constants'; import { Header } from './header'; -import { type Corner } from './types' import Toolbar from './toolbar'; import { ResizeHandle } from './resize-handle'; -import { calculatePosition } from './helpers'; +import { calculatePosition, calculateBoundedSize, getBestCorner } from './helpers'; export const Widget = () => { - const inspectState = Store.inspectState.value; - const isInspectFocused = inspectState.kind === 'focused'; + const refShouldExpand = useRef(false); const refContainer = useRef(null); + const refContent = useRef(null); const refPropContainer = useRef(null); const refFooter = useRef(null); const refInitialMinimizedWidth = useRef(0); const refInitialMinimizedHeight = useRef(0); - useEffect(() => { - if (!refContainer.current || !refFooter.current) return; + const updateWidgetPosition = useCallback((shouldSave = true) => { + if (!refContainer.current) return; - refContainer.current.style.width = 'min-content'; - refInitialMinimizedHeight.current = refFooter.current.offsetHeight; - refInitialMinimizedWidth.current = refContainer.current.offsetWidth; + const inspectState = Store.inspectState.value; + const isInspectFocused = inspectState.kind === 'focused'; - signalWidget.value = { - ...signalWidget.value, - dimensions: { - ...signalWidget.value.dimensions, - width: refInitialMinimizedWidth.current, - height: refInitialMinimizedHeight.current + const { corner } = signalWidget.value; + let newWidth, newHeight; + + if (isInspectFocused) { + const lastDims = signalWidget.value.lastDimensions; + newWidth = calculateBoundedSize(lastDims.width, 0, true); + newHeight = calculateBoundedSize(lastDims.height, 0, false); + } else { + if (signalWidget.value.dimensions.width !== refInitialMinimizedWidth.current) { + signalWidget.value = { + ...signalWidget.value, + lastDimensions: { + ...signalWidget.value.dimensions + } + }; } + newWidth = refInitialMinimizedWidth.current; + newHeight = refInitialMinimizedHeight.current; + } + + const newPosition = calculatePosition(corner, newWidth, newHeight); + + if (newWidth < MIN_SIZE.width || newHeight < MIN_SIZE.height * 5) { + shouldSave = false; + } + + const container = refContainer.current; + const containerStyle = container.style; + + const onTransitionEnd = () => { + containerStyle.transition = 'none'; + + updateDimensions(); + container.removeEventListener('transitionend', onTransitionEnd); }; - signalRefContainer.value = refContainer.current; - }, []); + container.addEventListener('transitionend', onTransitionEnd); - const shouldExpand = useMemo(() => { - if (isInspectFocused && inspectState.focusedDomElement) { - const { parentCompositeFiber } = getCompositeComponentFromElement(inspectState.focusedDomElement); - if (!parentCompositeFiber) { - setTimeout(() => { - Store.inspectState.value = { - kind: 'inspect-off', - propContainer: refPropContainer.current!, - }; - }, 16); - - return false; - } + containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + + requestAnimationFrame(() => { + containerStyle.width = `${newWidth}px`; + containerStyle.height = `${newHeight}px`; + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + }); + + signalWidget.value = { + corner, + dimensions: { + isFullWidth: newWidth >= window.innerWidth - (SAFE_AREA * 2), + isFullHeight: newHeight >= window.innerHeight - (SAFE_AREA * 2), + width: newWidth, + height: newHeight, + position: newPosition + }, + lastDimensions: signalWidget.value.lastDimensions + }; + + if (shouldSave) { + saveLocalStorage(LOCALSTORAGE_KEY, { + corner: signalWidget.value.corner, + dimensions: signalWidget.value.dimensions, + lastDimensions: signalWidget.value.lastDimensions + }); } - return true; - }, [isInspectFocused]); + updateDimensions(); + }, []); - const handleMouseDown = useCallback((e: JSX.TargetedMouseEvent) => { - if ((e.target as HTMLElement).closest('button')) return; - if (!refContainer.current) return; + const handleMouseDown = useCallback((e: JSX.TargetedMouseEvent) => { e.preventDefault(); + if (!refContainer.current || (e.target as HTMLElement).closest('button')) return; + const container = refContainer.current; + const containerStyle = container.style; const { dimensions } = signalWidget.value; - const startX = e.clientX; - const startY = e.clientY; + + const initialMouseX = e.clientX; + const initialMouseY = e.clientY; + const initialX = dimensions.position.x; const initialY = dimensions.position.y; let currentX = initialX; let currentY = initialY; let rafId: number | null = null; + let hasMoved = false; + let lastMouseX = initialMouseX; + let lastMouseY = initialMouseY; const handleMouseMove = (e: globalThis.MouseEvent) => { if (rafId) return; - container.style.transition = 'none'; + hasMoved = true; + lastMouseX = e.clientX; + lastMouseY = e.clientY; rafId = requestAnimationFrame(() => { - const deltaX = e.clientX - startX; - const deltaY = e.clientY - startY; + const deltaX = lastMouseX - initialMouseX; + const deltaY = lastMouseY - initialMouseY; - currentX = initialX + deltaX; - currentY = initialY + deltaY; + currentX = Number(initialX) + deltaX; + currentY = Number(initialY) + deltaY; - container.style.transform = `translate(${currentX}px, ${currentY}px)`; + containerStyle.transition = 'none'; + containerStyle.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; rafId = null; }); }; @@ -98,123 +144,146 @@ export const Widget = () => { if (!container) return; if (rafId) cancelAnimationFrame(rafId); - requestAnimationFrame(() => { - container.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); - const newCorner = `${currentY < window.innerHeight / 2 ? 'top' : 'bottom'}-${currentX < window.innerWidth / 2 ? 'left' : 'right'}` as Corner; - const snappedPosition = calculatePosition(newCorner, dimensions.width, dimensions.height); + if (!hasMoved) return; + + const newCorner = getBestCorner( + lastMouseX, + lastMouseY, + initialMouseX, + initialMouseY, + Store.inspectState.value.kind === 'focused' ? 80 : 40 + ); + + if (newCorner === signalWidget.value.corner) { + containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + const currentPosition = signalWidget.value.dimensions.position; + requestAnimationFrame(() => { + containerStyle.transform = `translate3d(${currentPosition.x}px, ${currentPosition.y}px, 0)`; + }); + return; + } - if (currentX === initialX && currentY === initialY) return; + const snappedPosition = calculatePosition(newCorner, dimensions.width, dimensions.height); - container.style.transform = `translate(${snappedPosition.x}px, ${snappedPosition.y}px)`; + if (currentX === initialX && currentY === initialY) return; - signalWidget.value = { - isResizing: false, - corner: newCorner, - dimensions: { - isFullWidth: dimensions.isFullWidth, - isFullHeight: dimensions.isFullHeight, - width: dimensions.width, - height: dimensions.height, - position: snappedPosition - }, - lastDimensions: signalWidget.value.lastDimensions - }; + const onTransitionEnd = () => { + containerStyle.transition = ''; + updateDimensions(); + container.removeEventListener('transitionend', onTransitionEnd); + }; + + container.addEventListener('transitionend', onTransitionEnd); + + containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + + requestAnimationFrame(() => { + containerStyle.transform = `translate3d(${snappedPosition.x}px, ${snappedPosition.y}px, 0)`; + }); + signalWidget.value = { + corner: newCorner, + dimensions: { + isFullWidth: dimensions.isFullWidth, + isFullHeight: dimensions.isFullHeight, + width: dimensions.width, + height: dimensions.height, + position: snappedPosition + }, + lastDimensions: signalWidget.value.lastDimensions + }; + + const shouldSave = !(dimensions.width < MIN_SIZE.width || dimensions.height < MIN_SIZE.height * 5); + if (shouldSave) { saveLocalStorage(LOCALSTORAGE_KEY, { corner: signalWidget.value.corner, dimensions: signalWidget.value.dimensions, lastDimensions: signalWidget.value.lastDimensions }); - - updateDimensions(); - }); - - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + } }; - document.addEventListener('mousemove', handleMouseMove, { passive: true }); + document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }, []); useEffect(() => { - if (!refContainer.current || !shouldExpand) { - return; - } - - let resizeTimeout: number; - let isInitialUpdate = true; - - const updateWidgetPosition = () => { - if (!refContainer.current) return; + if (!refContainer.current || !refFooter.current) return; - const { corner } = signalWidget.value; - let newWidth, newHeight; + refContainer.current.style.width = 'min-content'; + refInitialMinimizedHeight.current = refFooter.current.offsetHeight; + refInitialMinimizedWidth.current = refContainer.current.offsetWidth; - if (isInspectFocused) { - const lastDims = signalWidget.value.lastDimensions; - newWidth = lastDims.width; - newHeight = lastDims.height; - } else { - if (signalWidget.value.dimensions.width !== refInitialMinimizedWidth.current) { - signalWidget.value = { - ...signalWidget.value, - lastDimensions: { - ...signalWidget.value.dimensions - } - }; - } - newWidth = refInitialMinimizedWidth.current; - newHeight = refInitialMinimizedHeight.current; + signalWidget.value = { + ...signalWidget.value, + dimensions: { + ...signalWidget.value.dimensions, + width: refInitialMinimizedWidth.current, + height: refInitialMinimizedHeight.current } + }; - const newPosition = calculatePosition(corner, newWidth, newHeight); + signalRefContainer.value = refContainer.current; - refContainer.current.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; - refContainer.current.style.width = `${newWidth}px`; - refContainer.current.style.height = `${newHeight}px`; - refContainer.current.style.transform = `translate(${newPosition.x}px, ${newPosition.y}px)`; + const unsubscribeSignalWidget = signalWidget.subscribe(widget => { + if (!refContainer.current) return; - signalWidget.value = { - isResizing: false, - corner, - dimensions: { - isFullWidth: newWidth >= window.innerWidth - (SAFE_AREA * 2), - isFullHeight: newHeight >= window.innerHeight - (SAFE_AREA * 2), - width: newWidth, - height: newHeight, - position: newPosition - }, - lastDimensions: signalWidget.value.lastDimensions - }; + const { x, y } = widget.dimensions.position; + const { width, height } = widget.dimensions; + const container = refContainer.current; - if (!isInitialUpdate) { - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: signalWidget.value.corner, - dimensions: signalWidget.value.dimensions, - lastDimensions: signalWidget.value.lastDimensions - }); + requestAnimationFrame(() => { + container.style.transform = `translate3d(${x}px, ${y}px, 0)`; + container.style.width = `${width}px`; + container.style.height = `${height}px`; + }); + }); + + const unsubscribeStoreInspectState = Store.inspectState.subscribe(state => { + if (!refContent.current || !refPropContainer.current) return; + + refShouldExpand.current = state.kind === 'focused'; + + if (state.kind === 'focused') { + const { parentCompositeFiber } = getCompositeComponentFromElement(state.focusedDomElement); + if (!parentCompositeFiber) { + setTimeout(() => { + Store.inspectState.value = { + kind: 'inspect-off', + propContainer: refPropContainer.current!, + }; + }, 16); + } + } else { + toggleMultipleClasses(refContent.current, 'opacity-0', 'duration-0', 'delay-0'); } - isInitialUpdate = false; + updateWidgetPosition(); + }); - updateDimensions(); - }; + let resizeTimeout: number; const handleWindowResize = debounce(() => { - if (resizeTimeout) window.cancelAnimationFrame(resizeTimeout); - resizeTimeout = window.requestAnimationFrame(updateWidgetPosition); - }, 16); - - updateWidgetPosition(); + if (resizeTimeout) cancelAnimationFrame(resizeTimeout); + resizeTimeout = requestAnimationFrame(() => { + const container = refContainer.current; + if (!container) return; + updateWidgetPosition(); + }); + }, 32); window.addEventListener('resize', handleWindowResize); + updateWidgetPosition(false); + return () => { window.removeEventListener('resize', handleWindowResize); - if (resizeTimeout) window.cancelAnimationFrame(resizeTimeout); + unsubscribeStoreInspectState(); + unsubscribeSignalWidget(); }; - }, [isInspectFocused, shouldExpand]); + }, []); return (
{ 'cursor-move', 'z-[124124124124]', 'animate-fade-in animation-duration-300 animation-delay-300', + 'will-change-transform', )} >
{ "overflow-hidden", 'opacity-100', 'transition-opacity duration-150', - { - 'opacity-0 duration-0 delay-0': !isInspectFocused, - } )} >
@@ -255,9 +323,6 @@ export const Widget = () => { "text-white", "transition-opacity duration-150 delay-150", "overflow-y-auto overflow-x-hidden", - { - 'opacity-0 duration-0 delay-0': !isInspectFocused, - } )} />
@@ -275,21 +340,15 @@ export const Widget = () => {
- { - isInspectFocused && shouldExpand && ( - <> - - - - - - - - - - - ) - } + + + + + + + + +
); }; diff --git a/packages/scan/src/core/web/components/widget/resize-handle.tsx b/packages/scan/src/core/web/components/widget/resize-handle.tsx index d77705a4..10d87b8e 100644 --- a/packages/scan/src/core/web/components/widget/resize-handle.tsx +++ b/packages/scan/src/core/web/components/widget/resize-handle.tsx @@ -1,138 +1,103 @@ import { type JSX } from 'preact'; -import { useCallback } from "preact/hooks"; -import { memo } from 'preact/compat'; -import { cn, saveLocalStorage } from '@web-utils/helpers'; -import { Icon } from '@web-components/icon'; -import { signalWidget, signalRefContainer, updateDimensions } from '../../state'; -import { LOCALSTORAGE_KEY, MIN_SIZE, SAFE_AREA } from '../../constants'; -import { type ResizeHandleProps } from './types'; +import { useCallback, useRef, useEffect } from 'preact/hooks'; +import { cn, saveLocalStorage, toggleMultipleClasses } from '@web-utils/helpers'; +import { Store } from 'src/core'; +import { signalWidget, signalRefContainer } from '../../state'; +import { SAFE_AREA, LOCALSTORAGE_KEY, MIN_SIZE } from '../../constants'; +import { Icon } from '../icon'; +import { type Corner, type ResizeHandleProps } from './types'; import { - calculatePosition, - getInteractionClasses, - getPositionClasses, - calculateNewSizeAndPosition, - getHandleVisibility, getWindowDimensions, getOppositeCorner, - getClosestCorner + getClosestCorner, + calculateNewSizeAndPosition, + getPositionClasses, + getInteractionClasses, + getHandleVisibility, + calculatePosition } from './helpers'; -export const ResizeHandle = memo(({ position }: ResizeHandleProps) => { +export const ResizeHandle = ({ position }: ResizeHandleProps) => { const isLine = !position.includes('-'); - const { dimensions, lastDimensions } = signalWidget.value; - const { isFullWidth, isFullHeight } = dimensions; - const currentCorner = signalWidget.value.corner; - - const getVisibilityClass = useCallback(() => - getHandleVisibility(position, isLine, currentCorner, isFullWidth, isFullHeight), - [dimensions, position, isLine, isFullWidth, isFullHeight] - ); - - const handleDoubleClick = useCallback((e: JSX.TargetedMouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (!signalRefContainer.value) return; - - const { maxWidth, maxHeight, isFullWidth, isFullHeight } = getWindowDimensions(); - const currentWidth = signalWidget.value.dimensions.width; - const currentHeight = signalWidget.value.dimensions.height; - const currentCorner = signalWidget.value.corner; - - let isCurrentFullWidth = isFullWidth(currentWidth); - let isCurrentFullHeight = isFullHeight(currentHeight); - const isFullScreen = isCurrentFullWidth && isCurrentFullHeight; - const isPartiallyMaximized = (isCurrentFullWidth || isCurrentFullHeight) && !isFullScreen; - - let newWidth = currentWidth; - let newHeight = currentHeight; - const newCorner = getOppositeCorner( - position, - currentCorner, - isFullScreen, - isCurrentFullWidth, - isCurrentFullHeight - ); - - if (isLine) { - if (position === 'left' || position === 'right') { - newWidth = isCurrentFullWidth ? lastDimensions.width : maxWidth; - if (isPartiallyMaximized) { - newWidth = isCurrentFullWidth ? MIN_SIZE.width : maxWidth; - } + const refContainer = useRef(null); + const refLine = useRef(null); + const refCorner = useRef(null); + + const prevWidth = useRef(null); + const prevHeight = useRef(null); + const prevCorner = useRef(null); + + useEffect(() => { + if (!refContainer.current) return; + const classes = getInteractionClasses(position, isLine); + toggleMultipleClasses(refContainer.current, ...classes); + + const updateVisibility = (isFocused: boolean) => { + if (!refContainer.current) return; + const isVisible = isFocused && getHandleVisibility( + position, + isLine, + signalWidget.value.corner, + signalWidget.value.dimensions.isFullWidth, + signalWidget.value.dimensions.isFullHeight + ); + + if (!isVisible) { + refContainer.current.classList.add('hidden', 'pointer-events-none', 'opacity-0'); } else { - newHeight = isCurrentFullHeight ? lastDimensions.height : maxHeight; - if (isPartiallyMaximized) { - newHeight = isCurrentFullHeight ? MIN_SIZE.height * 12 : maxHeight; - } + refContainer.current.classList.remove('hidden', 'pointer-events-none', 'opacity-0'); } - } else { - newWidth = maxWidth; - newHeight = maxHeight; - } - + }; - if (isFullScreen) { - if (isLine) { - if (position === 'left' || position === 'right') { - newWidth = MIN_SIZE.width; - } else { - newHeight = MIN_SIZE.height * 12; - } - } else { - newWidth = MIN_SIZE.width; - newHeight = MIN_SIZE.height * 12; - isCurrentFullWidth = false; - isCurrentFullHeight = false; + const unsubscribeSignalWidget = signalWidget.subscribe((state) => { + if (!refContainer.current) return; + + if ( + prevWidth.current !== null && + prevHeight.current !== null && + prevCorner.current !== null && + state.dimensions.width === prevWidth.current && + state.dimensions.height === prevHeight.current && + state.corner === prevCorner.current + ) { + return; } - } - - const container = signalRefContainer.value; - const newPosition = calculatePosition(newCorner, newWidth, newHeight); - container.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; - container.style.width = `${newWidth}px`; - container.style.height = `${newHeight}px`; - container.style.transform = `translate(${newPosition.x}px, ${newPosition.y}px)`; + updateVisibility(Store.inspectState.value.kind === 'focused'); - const newState = { - isResizing: true, - corner: newCorner, - dimensions: { - isFullWidth: isCurrentFullWidth, - isFullHeight: isCurrentFullHeight, - width: newWidth, - height: newHeight, - position: newPosition - }, - lastDimensions: signalWidget.value.dimensions - }; - signalWidget.value = newState; + prevWidth.current = state.dimensions.width; + prevHeight.current = state.dimensions.height; + prevCorner.current = state.corner; + }); - const onTransitionEnd = () => { - container.style.transition = ''; - updateDimensions(); - container.removeEventListener('transitionend', onTransitionEnd); - }; - container.addEventListener('transitionend', onTransitionEnd); + const unsubscribeStoreInspectState = Store.inspectState.subscribe(state => { + if (!refContainer.current) return; - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: newCorner, - dimensions: signalWidget.value.dimensions + updateVisibility(state.kind === 'focused'); }); - }, [isLine, position, lastDimensions]); + + return () => { + unsubscribeSignalWidget(); + unsubscribeStoreInspectState(); + prevWidth.current = null; + prevHeight.current = null; + prevCorner.current = null; + }; + }, []); const handleResize = useCallback((e: JSX.TargetedMouseEvent) => { e.preventDefault(); e.stopPropagation(); + const container = signalRefContainer.value; if (!container) return; + const containerStyle = container.style; + const { dimensions } = signalWidget.value; const initialX = e.clientX; const initialY = e.clientY; - const { dimensions } = signalWidget.value; const initialWidth = dimensions.isFullWidth ? window.innerWidth - (SAFE_AREA * 2) @@ -143,12 +108,9 @@ export const ResizeHandle = memo(({ position }: ResizeHandleProps) => { const initialPosition = dimensions.position; let rafId: number | null = null; - let isResizing = true; const handleMouseMove = (e: MouseEvent) => { - if (!isResizing || rafId) return; - - container.style.transition = 'none'; + if (rafId) return; rafId = requestAnimationFrame(() => { const { newSize, newPosition } = calculateNewSizeAndPosition( @@ -159,9 +121,9 @@ export const ResizeHandle = memo(({ position }: ResizeHandleProps) => { e.clientY - initialY ); - container.style.width = `${newSize.width}px`; - container.style.height = `${newSize.height}px`; - container.style.transform = `translate(${newPosition.x}px, ${newPosition.y}px)`; + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + containerStyle.width = `${newSize.width}px`; + containerStyle.height = `${newSize.height}px`; signalWidget.value = { ...signalWidget.value, @@ -171,7 +133,6 @@ export const ResizeHandle = memo(({ position }: ResizeHandleProps) => { height: newSize.height, position: newPosition }, - isResizing: true }; rafId = null; @@ -179,143 +140,226 @@ export const ResizeHandle = memo(({ position }: ResizeHandleProps) => { }; const handleMouseUp = () => { - isResizing = false; if (rafId) cancelAnimationFrame(rafId); + const { dimensions, corner } = signalWidget.value; + const { isFullWidth, isFullHeight } = getWindowDimensions(); + const isCurrentFullWidth = isFullWidth(dimensions.width); + const isCurrentFullHeight = isFullHeight(dimensions.height); + const isFullScreen = isCurrentFullWidth && isCurrentFullHeight; + + let newCorner = corner; + if (isFullScreen || isCurrentFullWidth || isCurrentFullHeight) { + newCorner = getClosestCorner(dimensions.position); + } + + const newPosition = calculatePosition( + newCorner, + dimensions.width, + dimensions.height + ); + + const onTransitionEnd = () => { + container.removeEventListener('transitionend', onTransitionEnd); + }; + + container.addEventListener('transitionend', onTransitionEnd); + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + + signalWidget.value = { + corner: newCorner, + dimensions: { + isFullWidth: isCurrentFullWidth, + isFullHeight: isCurrentFullHeight, + width: dimensions.width, + height: dimensions.height, + position: newPosition + }, + lastDimensions: { + isFullWidth: isCurrentFullWidth, + isFullHeight: isCurrentFullHeight, + width: dimensions.width, + height: dimensions.height, + position: newPosition + } + }; + + saveLocalStorage(LOCALSTORAGE_KEY, { + corner: newCorner, + dimensions: signalWidget.value.dimensions, + lastDimensions: signalWidget.value.lastDimensions + }); + document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); + }; - requestAnimationFrame(() => { - const container = signalRefContainer.value; - if (!container) return; + document.addEventListener('mousemove', handleMouseMove, { passive: true }); + document.addEventListener('mouseup', handleMouseUp); + }, []); - const { dimensions, corner } = signalWidget.value; - const { isFullWidth, isFullHeight } = getWindowDimensions(); - const isCurrentFullWidth = isFullWidth(dimensions.width); - const isCurrentFullHeight = isFullHeight(dimensions.height); - const isFullScreen = isCurrentFullWidth && isCurrentFullHeight; + const handleDoubleClick = useCallback((e: JSX.TargetedMouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - let newCorner = corner; - if (isFullScreen || isCurrentFullWidth || isCurrentFullHeight) { - newCorner = getClosestCorner(dimensions.position); - } + const container = signalRefContainer.value; + if (!container) return; - const newPosition = calculatePosition( - newCorner, - dimensions.width, - dimensions.height - ); + const containerStyle = container.style; + const { dimensions, corner } = signalWidget.value; + const { maxWidth, maxHeight, isFullWidth, isFullHeight } = getWindowDimensions(); - container.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; - container.style.transform = `translate(${newPosition.x}px, ${newPosition.y}px)`; + const isCurrentFullWidth = isFullWidth(dimensions.width); + const isCurrentFullHeight = isFullHeight(dimensions.height); + const isFullScreen = isCurrentFullWidth && isCurrentFullHeight; + const isPartiallyMaximized = (isCurrentFullWidth || isCurrentFullHeight) && !isFullScreen; - signalWidget.value = { - isResizing: false, - corner: newCorner, - dimensions: { - isFullWidth: isCurrentFullWidth, - isFullHeight: isCurrentFullHeight, - width: dimensions.width, - height: dimensions.height, - position: newPosition - }, - lastDimensions: { - isFullWidth: isCurrentFullWidth, - isFullHeight: isCurrentFullHeight, - width: dimensions.width, - height: dimensions.height, - position: newPosition - } - }; + let newWidth = dimensions.width; + let newHeight = dimensions.height; + const newCorner = getOppositeCorner( + position, + corner, + isFullScreen, + isCurrentFullWidth, + isCurrentFullHeight + ); - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: newCorner, - dimensions: signalWidget.value.dimensions, - lastDimensions: signalWidget.value.lastDimensions - }); + if (isLine) { + if (position === 'left' || position === 'right') { + newWidth = isCurrentFullWidth ? dimensions.width : maxWidth; + if (isPartiallyMaximized) { + newWidth = isCurrentFullWidth ? MIN_SIZE.width : maxWidth; + } + } else { + newHeight = isCurrentFullHeight ? dimensions.height : maxHeight; + if (isPartiallyMaximized) { + newHeight = isCurrentFullHeight ? MIN_SIZE.height * 5 : maxHeight; + } + } + } else { + newWidth = maxWidth; + newHeight = maxHeight; + } - updateDimensions(); - }); + if (isFullScreen) { + if (isLine) { + if (position === 'left' || position === 'right') { + newWidth = MIN_SIZE.width; + } else { + newHeight = MIN_SIZE.height * 5; + } + } else { + newWidth = MIN_SIZE.width; + newHeight = MIN_SIZE.height * 5; + } + } + + const newPosition = calculatePosition(newCorner, newWidth, newHeight); + const newDimensions = { + isFullWidth: isFullWidth(newWidth), + isFullHeight: isFullHeight(newHeight), + width: newWidth, + height: newHeight, + position: newPosition }; - document.addEventListener('mousemove', handleMouseMove, { passive: true }); - document.addEventListener('mouseup', handleMouseUp, { passive: true }); - }, [position]); + requestAnimationFrame(() => { + signalWidget.value = { + corner: newCorner, + dimensions: newDimensions, + lastDimensions: dimensions + }; + + containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + containerStyle.width = `${newWidth}px`; + containerStyle.height = `${newHeight}px`; + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + }); + + saveLocalStorage(LOCALSTORAGE_KEY, { + corner: newCorner, + dimensions: newDimensions, + lastDimensions: dimensions + }); + }, []); return (
{ - isLine ? - ( - + - - - ) + /> + + ) : ( + > + + ) }
); -}, (prevProps, nextProps) => prevProps.position === nextProps.position); +}; diff --git a/packages/scan/src/core/web/components/widget/toolbar.tsx b/packages/scan/src/core/web/components/widget/toolbar.tsx index 42b9583f..cbc373e8 100644 --- a/packages/scan/src/core/web/components/widget/toolbar.tsx +++ b/packages/scan/src/core/web/components/widget/toolbar.tsx @@ -1,7 +1,6 @@ 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 { ReactScanInternals, Store, setOptions } from '../../../..'; import { getNearestFiberFromElement } from '../../inspect-element/utils'; import { Icon } from '../icon'; import { FpsMeter } from './fps-meter'; @@ -140,7 +139,8 @@ export const Toolbar = ({ refPropContainer }: ToolbarProps) => { const onSoundToggle = useCallback(() => { const newSoundState = !ReactScanInternals.options.value.playSound; - setOptions({ playSound: newSoundState }); + setOptions({ playSound: newSoundState, showToolbar: true }); + saveLocalStorage('react-scan-sound', newSoundState); }, []); useEffect(() => { @@ -155,10 +155,15 @@ export const Toolbar = ({ refPropContainer }: ToolbarProps) => { }, []); useEffect(() => { - const savedPausedState = readLocalStorage('react-scan-paused'); - if (savedPausedState !== null) { - if (instrumentation) { - instrumentation.isPaused.value = savedPausedState; + if (instrumentation) { + const savedStatePaused = readLocalStorage('react-scan-paused'); + if (savedStatePaused !== null) { + instrumentation.isPaused.value = savedStatePaused; + } + + const savedStateSound = readLocalStorage('react-scan-sound'); + if (savedStateSound !== null) { + setOptions({ playSound: savedStateSound, showToolbar: true }); } } }, []); @@ -166,7 +171,6 @@ export const Toolbar = ({ refPropContainer }: ToolbarProps) => { return (
- {isInspectFocused && ( -
- - -
- )} + + +
+ ) + }
-
react-scan
+ react-scan
diff --git a/packages/scan/src/core/web/components/widget/types.ts b/packages/scan/src/core/web/components/widget/types.ts index b93fd78e..d6ed4a26 100644 --- a/packages/scan/src/core/web/components/widget/types.ts +++ b/packages/scan/src/core/web/components/widget/types.ts @@ -23,7 +23,6 @@ export interface WidgetDimensions { } export interface WidgetConfig { - isResizing: boolean; corner: Corner; dimensions: WidgetDimensions; lastDimensions: WidgetDimensions; diff --git a/packages/scan/src/core/web/inspect-element/inspect-state-machine.ts b/packages/scan/src/core/web/inspect-element/inspect-state-machine.ts index 133e313a..81dee8e6 100644 --- a/packages/scan/src/core/web/inspect-element/inspect-state-machine.ts +++ b/packages/scan/src/core/web/inspect-element/inspect-state-machine.ts @@ -30,8 +30,7 @@ export type States = propContainer?: HTMLDivElement; }; -export const INSPECT_TOGGLE_ID = 'react-scan-inspect-element-toggle'; -export const INSPECT_OVERLAY_CANVAS_ID = 'react-scan-inspect-canvas'; +const INSPECT_OVERLAY_CANVAS_ID = 'react-scan-inspect-canvas'; let lastHoveredElement: HTMLElement; let animationId: ReturnType; @@ -302,7 +301,7 @@ export const createInspectElementStateMachine = (shadow: ShadowRoot) => { 'inspecting', ); - inspectState.propContainer.innerHTML = ''; + // inspectState.propContainer.innerHTML = ''; Store.inspectState.value = { kind: 'inspecting', hoveredDomElement: diff --git a/packages/scan/src/core/web/state.ts b/packages/scan/src/core/web/state.ts index 06560814..f182e4df 100644 --- a/packages/scan/src/core/web/state.ts +++ b/packages/scan/src/core/web/state.ts @@ -8,7 +8,6 @@ export const signalRefContainer = signal(null); const getInitialWidgetConfig = (): WidgetConfig => { if (typeof window === 'undefined') { return { - isResizing: false, corner: 'top-left' as Corner, dimensions: { isFullWidth: false, @@ -30,7 +29,6 @@ const getInitialWidgetConfig = (): WidgetConfig => { const stored = readLocalStorage(LOCALSTORAGE_KEY); if (!stored) { const defaultConfig: WidgetConfig = { - isResizing: false, corner: 'top-left' as Corner, dimensions: { isFullWidth: false, @@ -58,7 +56,6 @@ const getInitialWidgetConfig = (): WidgetConfig => { } return { - isResizing: false, corner: stored.corner, dimensions: stored.dimensions, lastDimensions: stored.lastDimensions diff --git a/packages/scan/src/core/web/utils/helpers.ts b/packages/scan/src/core/web/utils/helpers.ts index 31e22980..b3e85a31 100644 --- a/packages/scan/src/core/web/utils/helpers.ts +++ b/packages/scan/src/core/web/utils/helpers.ts @@ -100,3 +100,7 @@ export const saveLocalStorage = (storageKey: string, state: T): | void => { // Silently fail } }; + +export const toggleMultipleClasses = (element: HTMLElement, ...classes: Array) => { + classes.forEach(cls => element.classList.toggle(cls)); +}; diff --git a/packages/scan/tailwind.config.mjs b/packages/scan/tailwind.config.mjs index b8c10725..42de4b57 100644 --- a/packages/scan/tailwind.config.mjs +++ b/packages/scan/tailwind.config.mjs @@ -9,6 +9,9 @@ export default { fontFamily: { mono: ['Menlo', 'Consolas', 'Monaco', 'Liberation Mono', 'Lucida Console', 'monospace'], }, + fontSize: { + 'xxs': '0.5rem', + }, cursor: { 'nwse-resize': 'nwse-resize', 'nesw-resize': 'nesw-resize', diff --git a/packages/website/app/layout.tsx b/packages/website/app/layout.tsx index 85c53b3c..e753608b 100644 --- a/packages/website/app/layout.tsx +++ b/packages/website/app/layout.tsx @@ -69,9 +69,13 @@ export default function RootLayout({ -