From 73a6aee543ed1b45fca2f69449f1635dc145f152 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 8 Jan 2025 21:18:09 +0800 Subject: [PATCH] perf cleanup: toolbar (#181) * Fix format * Remove unnecessary useMemo * Update index.tsx * Update index.tsx * Update helpers.ts * Update resize-handle.tsx * Update resize-handle.tsx * Update utils.ts * Update index.tsx * Fix `getDisplayName` --- packages/scan/src/core/monitor/performance.ts | 2 +- .../components/copy-to-clipboard/index.tsx | 128 +++--- .../scan/src/web/components/icon/index.tsx | 80 ++-- .../src/web/components/inspector/index.tsx | 168 ++++---- .../components/inspector/overlay/index.tsx | 9 +- .../web/components/inspector/overlay/utils.ts | 9 +- .../src/web/components/inspector/utils.ts | 79 ++-- .../scan/src/web/components/widget/header.tsx | 19 +- .../scan/src/web/components/widget/helpers.ts | 244 ++++++----- .../scan/src/web/components/widget/index.tsx | 210 +++++----- .../web/components/widget/resize-handle.tsx | 381 +++++++++--------- .../web/components/widget/toolbar/search.tsx | 42 +- packages/scan/src/web/constants.ts | 2 +- packages/scan/src/web/state.ts | 28 +- packages/scan/src/web/utils/geiger.ts | 26 +- packages/scan/src/web/utils/outline-worker.ts | 28 +- .../scan/src/web/utils/preact/constant.ts | 1 - 17 files changed, 778 insertions(+), 678 deletions(-) diff --git a/packages/scan/src/core/monitor/performance.ts b/packages/scan/src/core/monitor/performance.ts index d9c2a739..75bd1b19 100644 --- a/packages/scan/src/core/monitor/performance.ts +++ b/packages/scan/src/core/monitor/performance.ts @@ -151,7 +151,7 @@ const getFirstNamedAncestorCompositeFiber = (element: Element) => { if (!fiber) { continue; } - if (getDisplayName(fiber?.type)) { + if (fiber.type && getDisplayName(fiber.type)) { parentCompositeFiber = fiber; } } diff --git a/packages/scan/src/web/components/copy-to-clipboard/index.tsx b/packages/scan/src/web/components/copy-to-clipboard/index.tsx index b67472e8..c3f8497e 100644 --- a/packages/scan/src/web/components/copy-to-clipboard/index.tsx +++ b/packages/scan/src/web/components/copy-to-clipboard/index.tsx @@ -1,82 +1,88 @@ -import { useRef, useState, useEffect, useCallback, useMemo } from "preact/hooks"; -import { memo } from "preact/compat"; -import { cn } from "~web/utils/helpers"; -import { Icon } from "../icon"; +import { memo } from 'preact/compat'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { cn } from '~web/utils/helpers'; +import { Icon } from '../icon'; interface CopyToClipboardProps { text: string; - children?: (props: { ClipboardIcon: JSX.Element; onClick: (e: MouseEvent) => void }) => JSX.Element; + children?: (props: { + ClipboardIcon: JSX.Element; + onClick: (e: MouseEvent) => void; + }) => JSX.Element; onCopy?: (success: boolean, text: string) => void; className?: string; iconSize?: number; } -export const CopyToClipboard = memo((props: CopyToClipboardProps): JSX.Element => { - const { +export const CopyToClipboard = memo( + ({ text, children, onCopy, className, iconSize = 14, - } = props; + }: CopyToClipboardProps): JSX.Element => { + const refTimeout = useRef(); + const [isCopied, setIsCopied] = useState(false); - const refTimeout = useRef(); - const [isCopied, setIsCopied] = useState(false); + useEffect(() => { + if (isCopied) { + refTimeout.current = setTimeout(() => setIsCopied(false), 600); + return () => { + clearTimeout(refTimeout.current); + }; + } + }, [isCopied]); - useEffect(() => { - if (isCopied) { - refTimeout.current = setTimeout(() => setIsCopied(false), 600); - return () => { - clearTimeout(refTimeout?.current); - }; - } - }, [isCopied]); - - const copyToClipboard = useCallback((e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const copyToClipboard = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - navigator.clipboard.writeText(text).then( - () => { - setIsCopied(true); - onCopy?.(true, text); - }, - () => { - onCopy?.(false, text); + navigator.clipboard.writeText(text).then( + () => { + setIsCopied(true); + onCopy?.(true, text); + }, + () => { + onCopy?.(false, text); + }, + ); }, + [text, onCopy], ); - }, [text, onCopy]); - const ClipboardIcon = useMemo((): JSX.Element => ( - - ), [className, copyToClipboard, iconSize, isCopied]); + const ClipboardIcon = ( + + ); - if (!children) { - return ClipboardIcon; - } + if (!children) { + return ClipboardIcon; + } - return children({ - ClipboardIcon, - onClick: copyToClipboard, - }); -}); + return children({ + ClipboardIcon, + onClick: copyToClipboard, + }); + }, +); diff --git a/packages/scan/src/web/components/icon/index.tsx b/packages/scan/src/web/components/icon/index.tsx index 7de46e38..0476444b 100644 --- a/packages/scan/src/web/components/icon/index.tsx +++ b/packages/scan/src/web/components/icon/index.tsx @@ -1,8 +1,5 @@ import type { JSX } from 'preact'; -import { - type ForwardedRef, - forwardRef, -} from 'preact/compat'; +import { forwardRef, type ForwardedRef } from 'preact/compat'; export interface SVGIconProps { size?: number | Array; @@ -14,43 +11,42 @@ export interface SVGIconProps { style?: JSX.CSSProperties; } -export const Icon = forwardRef((props: SVGIconProps, ref: ForwardedRef) => { - const { - size = 15, - name, - fill = 'currentColor', - stroke = 'currentColor', - className, - externalURL = '', - style, - } = props; +export const Icon = forwardRef( + ( + { + size = 15, + name, + fill = 'currentColor', + stroke = 'currentColor', + className, + externalURL = '', + style, + }: SVGIconProps, + ref: ForwardedRef, + ) => { + const width = Array.isArray(size) ? size[0] : size; + const height = Array.isArray(size) ? size[1] || size[0] : size; - const width = Array.isArray(size) ? size[0] : size; - const height = Array.isArray(size) ? size[1] || size[0] : size; + const path = `${externalURL}#${name}`; - const attributes = { - width: `${width}px`, - height: `${height}px`, - fill, - stroke, - className, - style, - }; - - const path = `${externalURL}#${name}`; - - return ( - - - - ); -}); + return ( + + + + ); + }, +); diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index 08401b5f..0dcd5397 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -166,7 +166,10 @@ const isExpandable = (value: unknown): value is InspectableValue => { }; const isPromise = (value: unknown): value is Promise => { - return !!value && (value instanceof Promise || (typeof value === 'object' && 'then' in value)); + return ( + !!value && + (value instanceof Promise || (typeof value === 'object' && 'then' in value)) + ); }; const isEditableValue = (value: unknown, parentPath?: string): boolean => { @@ -616,7 +619,11 @@ const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { ); }; -const updateNestedValue = (obj: unknown, path: Array, value: unknown): unknown => { +const updateNestedValue = ( + obj: unknown, + path: Array, + value: unknown, +): unknown => { try { if (path.length === 0) return value; @@ -645,12 +652,17 @@ const updateNestedValue = (obj: unknown, path: Array, value: unknown): u } if (obj && typeof obj === 'object') { + // TODO Megamorphic code if (rest.length === 0) { return { ...obj, [key]: value }; } return { ...obj, - [key]: updateNestedValue((obj as Record)[key], rest, value) + [key]: updateNestedValue( + (obj as Record)[key], + rest, + value, + ), }; } @@ -673,23 +685,15 @@ const PropertyElement = ({ const { fiber } = inspectorState.value; const refElement = useRef(null); - const [isExpanded, setIsExpanded] = useState(() => { - const currentPath = getPath( - getDisplayName(fiber?.type) ?? 'Unknown', - section, - parentPath ?? '', - name, - ); - return EXPANDED_PATHS.has(currentPath); - }); - const [isEditing, setIsEditing] = useState(false); const currentPath = getPath( - getDisplayName(fiber?.type) ?? 'Unknown', + (fiber?.type && getDisplayName(fiber.type)) ?? 'Unknown', section, parentPath ?? '', name, ); + const [isExpanded, setIsExpanded] = useState(EXPANDED_PATHS.has(currentPath)); + const [isEditing, setIsEditing] = useState(false); const prevValue = lastRendered.get(currentPath); const isChanged = prevValue !== undefined && !isEqual(prevValue, value); @@ -726,20 +730,22 @@ const PropertyElement = ({ ArrayBuffer.isView(obj) ); - return entries.map(([key, value]) => ( - - )); - }, [section, level, currentPath, objectPathMap, changedKeys]); + return entries.map(([key, value]) => ( + + )); + }, + [section, level, currentPath, objectPathMap, changedKeys], + ); const valuePreview = useMemo(() => formatValue(value), [value]); @@ -813,11 +819,12 @@ const PropertyElement = ({ } }, [canEdit]); - const handleSave = useCallback((newValue: unknown) => { - if (isEqual(value, newValue)) { - setIsEditing(false); - return; - } + const handleSave = useCallback( + (newValue: unknown) => { + if (isEqual(value, newValue)) { + setIsEditing(false); + return; + } if (section === 'props' && overrideProps) { tryOrElse(() => { @@ -902,26 +909,22 @@ const PropertyElement = ({ return (
- { - isExpandable(value) && ( - - ) - } + {isExpandable(value) && ( + + )}
)}
{name}:
- { - isEditing && isEditableValue(value, parentPath) - ? ( - setIsEditing(false)} - /> - ) - : ( - - ) - } + {isEditing && isEditableValue(value, parentPath) ? ( + setIsEditing(false)} + /> + ) : ( + + )} { const { changes } = inspectorState.value; const timerRef = useRef(); - const hasChanges = changes.state.size > 0 || changes.props.size > 0 || changes.context.size > 0; + const hasChanges = + changes.state.size > 0 || + changes.props.size > 0 || + changes.context.size > 0; useEffect(() => { if (hasChanges) { @@ -1076,12 +1074,9 @@ const WhatChanged = constant(() => { What changed?
{changes.state.size > 0 && ( @@ -1252,18 +1247,9 @@ export const Inspector = constant(() => {
- - - + + +
); diff --git a/packages/scan/src/web/components/inspector/overlay/index.tsx b/packages/scan/src/web/components/inspector/overlay/index.tsx index cce01279..8904e0fa 100644 --- a/packages/scan/src/web/components/inspector/overlay/index.tsx +++ b/packages/scan/src/web/components/inspector/overlay/index.tsx @@ -109,7 +109,8 @@ export const ScanOverlay = () => { const pillHeight = 24; const pillPadding = 8; - const componentName = getDisplayName(fiber?.type) ?? 'Unknown'; + const componentName = + (fiber?.type && getDisplayName(fiber.type)) ?? 'Unknown'; let text = componentName; if (stats.count) { text += ` • ×${stats.count}`; @@ -557,8 +558,9 @@ export const ScanOverlay = () => { const handleResizeOrScroll = () => { const state = Store.inspectState.peek(); const canvas = refCanvas.current; + if (!canvas) return; const ctx = canvas?.getContext('2d'); - if (!canvas || !ctx) return; + if (!ctx) return; cancelAnimationFrame(refRafId.current); clearTimeout(refTimeout.current); @@ -590,8 +592,9 @@ export const ScanOverlay = () => { useEffect(() => { const canvas = refCanvas.current; + if (!canvas) return; const ctx = canvas?.getContext('2d'); - if (!canvas || !ctx) return; + if (!ctx) return; updateCanvasSize(canvas, ctx); diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index ff2311d2..a2a4ece5 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -1,5 +1,5 @@ import { FunctionComponentTag } from 'bippy'; -import { Context, type ComponentState } from 'react'; +import { type ComponentState } from 'react'; import { type Fiber } from 'react-reconciler'; import { isEqual } from '~core/utils'; @@ -292,10 +292,8 @@ export const getChangedProps = (fiber: Fiber): Set => { if (!isEqual(currentValue, previousValue)) { changes.add(key); - if (typeof currentValue !== 'function') { - const count = (propsChangeCounts.get(key) ?? 0) + 1; - propsChangeCounts.set(key, count); - } + const count = (propsChangeCounts.get(key) ?? 0) + 1; + propsChangeCounts.set(key, count); } } @@ -409,7 +407,6 @@ export const getChangedContext = (fiber: Fiber): Set => { const currentContexts = getAllFiberContexts(fiber); for (const [contextName] of currentContexts) { - let searchFiber: Fiber | null = fiber; let providerFiber: Fiber | null = null; diff --git a/packages/scan/src/web/components/inspector/utils.ts b/packages/scan/src/web/components/inspector/utils.ts index e2e9ed87..6a9101e0 100644 --- a/packages/scan/src/web/components/inspector/utils.ts +++ b/packages/scan/src/web/components/inspector/utils.ts @@ -10,19 +10,19 @@ import { isEqual } from '~core/utils'; export type States = | { - kind: 'inspecting'; - hoveredDomElement: HTMLElement | null; - } + kind: 'inspecting'; + hoveredDomElement: HTMLElement | null; + } | { - kind: 'inspect-off'; - } + kind: 'inspect-off'; + } | { - kind: 'focused'; - focusedDomElement: HTMLElement; - } + kind: 'focused'; + focusedDomElement: HTMLElement; + } | { - kind: 'uninitialized'; - }; + kind: 'uninitialized'; + }; interface ReactRootContainer { _reactRootContainer?: { @@ -43,7 +43,12 @@ interface ReactRenderer { version: string; bundleType: number; rendererPackageName: string; - overrideHookState?: (fiber: Fiber, id: string, path: Array, value: any) => void; + overrideHookState?: ( + fiber: Fiber, + id: string, + path: Array, + value: any, + ) => void; overrideProps?: (fiber: Fiber, path: Array, value: any) => void; scheduleUpdate?: (fiber: Fiber) => void; } @@ -110,7 +115,9 @@ export const getFirstStateNode = (fiber: Fiber): Element | null => { return null; }; -export const getNearestFiberFromElement = (element: Element | null): Fiber | null => { +export const getNearestFiberFromElement = ( + element: Element | null, +): Fiber | null => { if (!element) return null; try { @@ -216,7 +223,7 @@ export const getChangedPropsDetailed = (fiber: Fiber): Array => { changes.push({ name: key, value: currentValue, - prevValue + prevValue, }); } } @@ -225,8 +232,12 @@ export const getChangedPropsDetailed = (fiber: Fiber): Array => { }; interface OverrideMethods { - overrideProps: ((fiber: Fiber, path: Array, value: unknown) => void) | null; - overrideHookState: ((fiber: Fiber, id: string, path: Array, value: unknown) => void) | null; + overrideProps: + | ((fiber: Fiber, path: Array, value: unknown) => void) + | null; + overrideHookState: + | ((fiber: Fiber, id: string, path: Array, value: unknown) => void) + | null; } export const getOverrideMethods = (): OverrideMethods => { @@ -240,7 +251,12 @@ export const getOverrideMethods = (): OverrideMethods => { try { if (overrideHookState) { const prevOverrideHookState = overrideHookState; - overrideHookState = (fiber: Fiber, id: string, path: Array, value: any) => { + overrideHookState = ( + fiber: Fiber, + id: string, + path: Array, + value: any, + ) => { // Find the hook let current = fiber.memoizedState; for (let i = 0; i < parseInt(id); i++) { @@ -283,7 +299,6 @@ export const getOverrideMethods = (): OverrideMethods => { return { overrideProps, overrideHookState }; }; - const nonVisualTags = new Set([ 'html', 'meta', @@ -306,12 +321,18 @@ const nonVisualTags = new Set([ 'slot', 'xml', 'doctype', - 'comment' + 'comment', ]); -export const findComponentDOMNode = (fiber: Fiber, excludeNonVisualTags = true): HTMLElement | null => { +export const findComponentDOMNode = ( + fiber: Fiber, + excludeNonVisualTags = true, +): HTMLElement | null => { if (fiber.stateNode && 'nodeType' in fiber.stateNode) { const element = fiber.stateNode as HTMLElement; - if (excludeNonVisualTags && nonVisualTags.has(element.tagName.toLowerCase())) { + if ( + excludeNonVisualTags && + nonVisualTags.has(element.tagName.toLowerCase()) + ) { return null; } return element; @@ -333,10 +354,14 @@ export interface InspectableElement { name: string; } -export const getInspectableElements = (root: HTMLElement = document.body): Array => { +export const getInspectableElements = ( + root: HTMLElement = document.body, +): Array => { const result: Array = []; - const findInspectableFiber = (element: HTMLElement | null): HTMLElement | null => { + const findInspectableFiber = ( + element: HTMLElement | null, + ): HTMLElement | null => { if (!element) return null; const { parentCompositeFiber } = getCompositeComponentFromElement(element); if (!parentCompositeFiber) return null; @@ -348,16 +373,20 @@ export const getInspectableElements = (root: HTMLElement = document.body): Array const traverse = (element: HTMLElement, depth = 0) => { const inspectable = findInspectableFiber(element); if (inspectable) { - const { parentCompositeFiber } = getCompositeComponentFromElement(inspectable); + const { parentCompositeFiber } = + getCompositeComponentFromElement(inspectable); result.push({ element: inspectable, depth, - name: getDisplayName(parentCompositeFiber!.type) ?? 'Unknown' + name: + (parentCompositeFiber!.type && + getDisplayName(parentCompositeFiber!.type)) ?? + 'Unknown', }); } // Traverse children first (depth-first) - Array.from(element.children).forEach(child => { + Array.from(element.children).forEach((child) => { traverse(child as HTMLElement, inspectable ? depth + 1 : depth); }); }; diff --git a/packages/scan/src/web/components/widget/header.tsx b/packages/scan/src/web/components/widget/header.tsx index c18f944c..56addc46 100644 --- a/packages/scan/src/web/components/widget/header.tsx +++ b/packages/scan/src/web/components/widget/header.tsx @@ -1,12 +1,12 @@ -import { useRef, useEffect } from 'preact/hooks'; import { getDisplayName } from 'bippy'; +import { useEffect, useRef } from 'preact/hooks'; import { Store } from '~core/index'; import { replayComponent } from '~web/components/inspector'; +import { Icon } from '../icon'; import { getCompositeComponentFromElement, getOverrideMethods, } from '../inspector/utils'; -import { Icon } from '../icon'; const REPLAY_DELAY_MS = 300; @@ -90,12 +90,12 @@ export const Header = () => { const refMetrics = useRef(null); useSubscribeFocusedFiber(() => { - cancelAnimationFrame(refRaf.current ?? 0); refRaf.current = requestAnimationFrame(() => { if (Store.inspectState.value.kind !== 'focused') return; const focusedElement = Store.inspectState.value.focusedDomElement; - const { parentCompositeFiber } = getCompositeComponentFromElement(focusedElement); + const { parentCompositeFiber } = + getCompositeComponentFromElement(focusedElement); if (!parentCompositeFiber) return; const displayName = getDisplayName(parentCompositeFiber.type); @@ -105,11 +105,12 @@ export const Header = () => { if (refComponentName.current && refMetrics.current) { refComponentName.current.dataset.text = displayName ?? 'Unknown'; - const formattedTime = time > 0 - ? time < 0.1 - Number.EPSILON - ? '< 0.1ms' - : `${Number(time.toFixed(1))}ms` - : ''; + const formattedTime = + time > 0 + ? time < 0.1 - Number.EPSILON + ? '< 0.1ms' + : `${Number(time.toFixed(1))}ms` + : ''; refMetrics.current.dataset.text = `${count} re-renders${formattedTime ? ` • ${formattedTime}` : ''}`; } diff --git a/packages/scan/src/web/components/widget/helpers.ts b/packages/scan/src/web/components/widget/helpers.ts index 39ae3d01..afdfa3f1 100644 --- a/packages/scan/src/web/components/widget/helpers.ts +++ b/packages/scan/src/web/components/widget/helpers.ts @@ -1,64 +1,65 @@ -import { SAFE_AREA, MIN_SIZE } from '../../constants'; -import { type Corner, type Position, type ResizeHandleProps, type Size } from './types'; - -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 = 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 - }; - } +import { MIN_SIZE, SAFE_AREA } from '../../constants'; +import { + type Corner, + type Position, + type ResizeHandleProps, + type Size, +} from './types'; + +class WindowDimensions { + maxWidth: number; + maxHeight: number; + + constructor( + public width: number, + public height: number, + ) { + this.maxWidth = width - SAFE_AREA * 2; + this.maxHeight = height - SAFE_AREA * 2; + } - const maxWidth = currentWidth - (SAFE_AREA * 2); - const maxHeight = currentHeight - (SAFE_AREA * 2); + rightEdge(width: number): number { + return this.width - width - SAFE_AREA; + } - 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 - }; - }; -})(); + bottomEdge(height: number): number { + return this.height - height - SAFE_AREA; + } + + isFullWidth(width: number): boolean { + return width >= this.maxWidth; + } + + isFullHeight(height: number): boolean { + return height >= this.maxHeight; + } +} + +let cachedWindowDimensions: WindowDimensions | undefined; + +export const getWindowDimensions = () => { + const currentWidth = window.innerWidth; + const currentHeight = window.innerHeight; + + if ( + cachedWindowDimensions && + cachedWindowDimensions.width === currentWidth && + cachedWindowDimensions.height === currentHeight + ) { + return cachedWindowDimensions; + } + + cachedWindowDimensions = new WindowDimensions(currentWidth, currentHeight); + + return cachedWindowDimensions; +}; export const getOppositeCorner = ( position: ResizeHandleProps['position'], currentCorner: Corner, isFullScreen: boolean, isFullWidth?: boolean, - isFullHeight?: boolean + isFullHeight?: boolean, ): Corner => { // For full screen mode if (isFullScreen) { @@ -76,20 +77,28 @@ export const getOppositeCorner = ( // For full width mode if (isFullWidth) { - if (position === 'left') return `${currentCorner.split('-')[0]}-right` as Corner; - if (position === 'right') return `${currentCorner.split('-')[0]}-left` as Corner; + if (position === 'left') + return `${currentCorner.split('-')[0]}-right` as Corner; + if (position === 'right') + return `${currentCorner.split('-')[0]}-left` as Corner; } // For full height mode if (isFullHeight) { - if (position === 'top') return `bottom-${currentCorner.split('-')[1]}` as Corner; - if (position === 'bottom') return `top-${currentCorner.split('-')[1]}` as Corner; + if (position === 'top') + return `bottom-${currentCorner.split('-')[1]}` as Corner; + if (position === 'bottom') + return `top-${currentCorner.split('-')[1]}` as Corner; } return currentCorner; }; -export const calculatePosition = (corner: Corner, width: number, height: number): Position => { +export const calculatePosition = ( + corner: Corner, + width: number, + height: number, +): Position => { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; @@ -97,8 +106,12 @@ export const calculatePosition = (corner: Corner, width: number, height: number) const isMinimized = width === MIN_SIZE.width; // Only bound dimensions if minimized - const effectiveWidth = isMinimized ? width : Math.min(width, windowWidth - (SAFE_AREA * 2)); - const effectiveHeight = isMinimized ? height : Math.min(height, windowHeight - (SAFE_AREA * 2)); + const effectiveWidth = isMinimized + ? width + : Math.min(width, windowWidth - SAFE_AREA * 2); + const effectiveHeight = isMinimized + ? height + : Math.min(height, windowHeight - SAFE_AREA * 2); // Calculate base positions let x: number; @@ -126,14 +139,23 @@ export const calculatePosition = (corner: Corner, width: number, height: number) // Only ensure positions are within bounds if minimized if (isMinimized) { - x = Math.max(SAFE_AREA, Math.min(x, windowWidth - effectiveWidth - SAFE_AREA)); - y = Math.max(SAFE_AREA, Math.min(y, windowHeight - effectiveHeight - SAFE_AREA)); + x = Math.max( + SAFE_AREA, + Math.min(x, windowWidth - effectiveWidth - SAFE_AREA), + ); + y = Math.max( + SAFE_AREA, + Math.min(y, windowHeight - effectiveHeight - SAFE_AREA), + ); } return { x, y }; }; -const positionMatchesCorner = (position: ResizeHandleProps['position'], corner: Corner): boolean => { +const positionMatchesCorner = ( + position: ResizeHandleProps['position'], + corner: Corner, +): boolean => { const [vertical, horizontal] = corner.split('-'); return position !== vertical && position !== horizontal; }; @@ -142,7 +164,7 @@ export const getHandleVisibility = ( position: ResizeHandleProps['position'], corner: Corner, isFullWidth: boolean, - isFullHeight: boolean + isFullHeight: boolean, ): boolean => { if (isFullWidth && isFullHeight) { return true; @@ -166,11 +188,10 @@ export const getHandleVisibility = ( return false; }; - export const calculateBoundedSize = ( currentSize: number, delta: number, - isWidth: boolean + isWidth: boolean, ): number => { const min = isWidth ? MIN_SIZE.width : MIN_SIZE.height * 5; const max = isWidth @@ -179,17 +200,17 @@ export const calculateBoundedSize = ( const newSize = currentSize + delta; return Math.min(Math.max(min, newSize), max); -} +}; export const calculateNewSizeAndPosition = ( position: ResizeHandleProps['position'], initialSize: Size, initialPosition: Position, deltaX: number, - deltaY: number + deltaY: number, ): { newSize: Size; newPosition: Position } => { - const maxWidth = window.innerWidth - (SAFE_AREA * 2); - const maxHeight = window.innerHeight - (SAFE_AREA * 2); + const maxWidth = window.innerWidth - SAFE_AREA * 2; + const maxHeight = window.innerHeight - SAFE_AREA * 2; let newWidth = initialSize.width; let newHeight = initialSize.height; @@ -215,40 +236,67 @@ export const calculateNewSizeAndPosition = ( if (position.includes('bottom')) { // 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)); + const proposedHeight = Math.min( + initialSize.height + deltaY, + availableHeight, + ); + newHeight = Math.min( + maxHeight, + Math.max(MIN_SIZE.height * 5, proposedHeight), + ); } if (position.includes('top')) { // 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)); + 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); } // Ensure position stays within bounds - newX = Math.max(SAFE_AREA, Math.min(newX, window.innerWidth - SAFE_AREA - newWidth)); - newY = Math.max(SAFE_AREA, Math.min(newY, window.innerHeight - SAFE_AREA - newHeight)); + newX = Math.max( + SAFE_AREA, + Math.min(newX, window.innerWidth - SAFE_AREA - newWidth), + ); + newY = Math.max( + SAFE_AREA, + Math.min(newY, window.innerHeight - SAFE_AREA - newHeight), + ); return { newSize: { width: newWidth, height: newHeight }, - newPosition: { x: newX, y: newY } + newPosition: { x: newX, y: newY }, }; }; export const getClosestCorner = (position: Position): Corner => { - const { maxWidth, maxHeight } = getWindowDimensions(); + const windowDims = getWindowDimensions(); - const distances = { + const distances: Record = { 'top-left': Math.hypot(position.x, position.y), - 'top-right': Math.hypot(maxWidth - position.x, position.y), - 'bottom-left': Math.hypot(position.x, maxHeight - position.y), - 'bottom-right': Math.hypot(maxWidth - position.x, maxHeight - position.y) + 'top-right': Math.hypot(windowDims.maxWidth - position.x, position.y), + 'bottom-left': Math.hypot(position.x, windowDims.maxHeight - position.y), + 'bottom-right': Math.hypot( + windowDims.maxWidth - position.x, + windowDims.maxHeight - position.y, + ), }; - return Object.entries(distances).reduce((closest, [corner, distance]) => { - return distance < distances[closest] ? corner as Corner : closest; - }, 'top-left'); + let closest: Corner = 'top-left'; + + for (const key in distances) { + if (distances[key as Corner] < distances[closest]) { + closest = key as Corner; + } + } + + return closest; }; // Helper to determine best corner based on cursor position, widget size, and movement @@ -257,7 +305,7 @@ export const getBestCorner = ( mouseY: number, initialMouseX?: number, initialMouseY?: number, - threshold = 100 + threshold = 100, ): Corner => { const deltaX = initialMouseX !== undefined ? mouseX - initialMouseX : 0; const deltaY = initialMouseY !== undefined ? mouseY - initialMouseY : 0; @@ -275,20 +323,32 @@ export const getBestCorner = ( if (movingRight || movingLeft) { const isBottom = mouseY > windowCenterY; return movingRight - ? (isBottom ? 'bottom-right' : 'top-right') - : (isBottom ? 'bottom-left' : 'top-left'); + ? 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'); + ? 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'); + ? mouseY > windowCenterY + ? 'bottom-right' + : 'top-right' + : mouseY > windowCenterY + ? 'bottom-left' + : 'top-left'; }; diff --git a/packages/scan/src/web/components/widget/index.tsx b/packages/scan/src/web/components/widget/index.tsx index a0e60fae..0a2d2bdb 100644 --- a/packages/scan/src/web/components/widget/index.tsx +++ b/packages/scan/src/web/components/widget/index.tsx @@ -1,9 +1,13 @@ import { type JSX } from 'preact'; import { useCallback, useEffect, useRef } from 'preact/hooks'; -import { saveLocalStorage, toggleMultipleClasses, debounce, cn } from '~web/utils/helpers'; -import { ScanOverlay } from '~web/components/inspector/overlay'; import { Store } from '~core/index'; -import { Inspector } from '../inspector'; +import { ScanOverlay } from '~web/components/inspector/overlay'; +import { + cn, + debounce, + saveLocalStorage, + toggleMultipleClasses, +} from '~web/utils/helpers'; import { LOCALSTORAGE_KEY, MIN_SIZE, SAFE_AREA } from '../../constants'; import { defaultWidgetConfig, @@ -11,6 +15,7 @@ import { signalWidget, updateDimensions, } from '../../state'; +import { Inspector } from '../inspector'; import { Header } from './header'; import { calculateBoundedSize, @@ -120,125 +125,130 @@ export const Widget = () => { updateDimensions(); }, []); - const handleDrag = 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 initialMouseX = e.clientX; - const initialMouseY = e.clientY; - - const initialX = dimensions.position.x; - const initialY = dimensions.position.y; + const handleDrag = useCallback( + (e: JSX.TargetedMouseEvent) => { + e.preventDefault(); - 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; + if (!refContainer.current || (e.target as HTMLElement).closest('button')) + return; - hasMoved = true; - lastMouseX = e.clientX; - lastMouseY = e.clientY; + const container = refContainer.current; + const containerStyle = container.style; + const { dimensions } = signalWidget.value; - rafId = requestAnimationFrame(() => { - const deltaX = lastMouseX - initialMouseX; - const deltaY = lastMouseY - initialMouseY; + const initialMouseX = e.clientX; + const initialMouseY = e.clientY; - currentX = Number(initialX) + deltaX; - currentY = Number(initialY) + deltaY; + const initialX = dimensions.position.x; + const initialY = dimensions.position.y; - containerStyle.transition = 'none'; - containerStyle.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; - rafId = null; - }); - }; + let currentX = initialX; + let currentY = initialY; + let rafId: number | null = null; + let hasMoved = false; + let lastMouseX = initialMouseX; + let lastMouseY = initialMouseY; - const handleMouseUp = () => { - if (!container) return; + const handleMouseMove = (e: globalThis.MouseEvent) => { + if (rafId) return; - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + hasMoved = true; + lastMouseX = e.clientX; + lastMouseY = e.clientY; - if (!hasMoved) return; + rafId = requestAnimationFrame(() => { + const deltaX = lastMouseX - initialMouseX; + const deltaY = lastMouseY - initialMouseY; - const newCorner = getBestCorner( - lastMouseX, - lastMouseY, - initialMouseX, - initialMouseY, - Store.inspectState.value.kind === 'focused' ? 80 : 40, - ); + currentX = Number(initialX) + deltaX; + currentY = Number(initialY) + deltaY; - 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)`; + containerStyle.transition = 'none'; + containerStyle.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; + rafId = null; }); - return; - } - - const snappedPosition = calculatePosition( - newCorner, - dimensions.width, - dimensions.height, - ); + }; - if (currentX === initialX && currentY === initialY) return; + const handleMouseUp = () => { + if (!container) return; - const onTransitionEnd = () => { - containerStyle.transition = 'none'; - updateDimensions(); - container.removeEventListener('transitionend', onTransitionEnd); if (rafId) { cancelAnimationFrame(rafId); rafId = null; } - }; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + + 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; + } - container.addEventListener('transitionend', onTransitionEnd); - containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + const snappedPosition = calculatePosition( + newCorner, + dimensions.width, + dimensions.height, + ); + + if (currentX === initialX && currentY === initialY) return; + + const onTransitionEnd = () => { + containerStyle.transition = 'none'; + updateDimensions(); + container.removeEventListener('transitionend', onTransitionEnd); + if (rafId) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; - requestAnimationFrame(() => { - containerStyle.transform = `translate3d(${snappedPosition.x}px, ${snappedPosition.y}px, 0)`; - }); + container.addEventListener('transitionend', onTransitionEnd); + containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; - signalWidget.value = { - corner: newCorner, - dimensions: { - isFullWidth: dimensions.isFullWidth, - isFullHeight: dimensions.isFullHeight, - width: dimensions.width, - height: dimensions.height, - position: snappedPosition, - }, - lastDimensions: signalWidget.value.lastDimensions, + 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, + }; + + saveLocalStorage(LOCALSTORAGE_KEY, { + corner: newCorner, + dimensions: signalWidget.value.dimensions, + lastDimensions: signalWidget.value.lastDimensions, + }); }; - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: newCorner, - dimensions: signalWidget.value.dimensions, - lastDimensions: signalWidget.value.lastDimensions, + document.addEventListener('mousemove', handleMouseMove, { + passive: true, }); - }; - - document.addEventListener('mousemove', handleMouseMove, { passive: true }); - document.addEventListener('mouseup', handleMouseUp); - }, []); + document.addEventListener('mouseup', handleMouseUp); + }, + [], + ); useEffect(() => { if (!refContainer.current || !refFooter.current) return; @@ -388,7 +398,7 @@ export const Widget = () => { 'transition-colors duration-200', 'overflow-hidden', 'rounded-lg', - 'z-10' + 'z-10', )} > diff --git a/packages/scan/src/web/components/widget/resize-handle.tsx b/packages/scan/src/web/components/widget/resize-handle.tsx index 23a7d342..e59cc210 100644 --- a/packages/scan/src/web/components/widget/resize-handle.tsx +++ b/packages/scan/src/web/components/widget/resize-handle.tsx @@ -1,5 +1,8 @@ import { type JSX } from 'preact'; import { useCallback, useEffect, useRef } from 'preact/hooks'; +import { Store } from '~core/index'; +import { Icon } from '~web/components/icon'; +import { cn, saveLocalStorage } from '~web/utils/helpers'; import { LOCALSTORAGE_KEY, MIN_SIZE } from '../../constants'; import { signalRefContainer, signalWidget } from '../../state'; import { @@ -11,9 +14,6 @@ import { getWindowDimensions, } from './helpers'; import { type Corner, type ResizeHandleProps } from './types'; -import { saveLocalStorage, cn } from '~web/utils/helpers'; -import { Icon } from '~web/components/icon'; -import { Store } from '~core/index'; export const ResizeHandle = ({ position }: ResizeHandleProps) => { const refContainer = useRef(null); @@ -23,10 +23,10 @@ export const ResizeHandle = ({ position }: ResizeHandleProps) => { const prevCorner = useRef(null); useEffect(() => { - if (!refContainer.current) return; + const container = refContainer.current; + if (!container) return; const updateVisibility = (isFocused: boolean) => { - if (!refContainer.current) return; const isVisible = isFocused && getHandleVisibility( @@ -37,23 +37,17 @@ export const ResizeHandle = ({ position }: ResizeHandleProps) => { ); if (isVisible) { - refContainer.current.classList.remove( + container.classList.remove( 'hidden', 'pointer-events-none', 'opacity-0', ); } else { - refContainer.current.classList.add( - 'hidden', - 'pointer-events-none', - 'opacity-0', - ); + container.classList.add('hidden', 'pointer-events-none', 'opacity-0'); } }; const unsubscribeSignalWidget = signalWidget.subscribe((state) => { - if (!refContainer.current) return; - if ( prevWidth.current !== null && prevHeight.current !== null && @@ -74,7 +68,6 @@ export const ResizeHandle = ({ position }: ResizeHandleProps) => { const unsubscribeStoreInspectState = Store.inspectState.subscribe( (state) => { - if (!refContainer.current) return; updateVisibility(state.kind === 'focused'); }, ); @@ -88,208 +81,217 @@ export const ResizeHandle = ({ position }: ResizeHandleProps) => { }; }, []); - const handleResize = useCallback((e: JSX.TargetedMouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleResize = useCallback( + (e: JSX.TargetedMouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - const container = signalRefContainer.value; - if (!container) return; + const container = signalRefContainer.value; + if (!container) return; - const containerStyle = container.style; - const { dimensions } = signalWidget.value; - const initialX = e.clientX; - const initialY = e.clientY; - - const initialWidth = dimensions.width; - const initialHeight = dimensions.height; - const initialPosition = dimensions.position; - - signalWidget.value = { - ...signalWidget.value, - dimensions: { - ...dimensions, - isFullWidth: false, - isFullHeight: false, - width: initialWidth, - height: initialHeight, - position: initialPosition, - }, - }; + const containerStyle = container.style; + const { dimensions } = signalWidget.value; + const initialX = e.clientX; + const initialY = e.clientY; - let rafId: number | null = null; + const initialWidth = dimensions.width; + const initialHeight = dimensions.height; + const initialPosition = dimensions.position; - const handleMouseMove = (e: MouseEvent) => { - if (rafId) return; + signalWidget.value = { + ...signalWidget.value, + dimensions: { + ...dimensions, + isFullWidth: false, + isFullHeight: false, + width: initialWidth, + height: initialHeight, + position: initialPosition, + }, + }; - containerStyle.transition = 'none'; + let rafId: number | null = null; + + const handleMouseMove = (e: MouseEvent) => { + if (rafId) return; + + containerStyle.transition = 'none'; + + rafId = requestAnimationFrame(() => { + const { newSize, newPosition } = calculateNewSizeAndPosition( + position, + { width: initialWidth, height: initialHeight }, + initialPosition, + e.clientX - initialX, + e.clientY - initialY, + ); + + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + containerStyle.width = `${newSize.width}px`; + containerStyle.height = `${newSize.height}px`; + + signalWidget.value = { + ...signalWidget.value, + dimensions: { + isFullWidth: false, + isFullHeight: false, + width: newSize.width, + height: newSize.height, + position: newPosition, + }, + }; + + rafId = null; + }); + }; - rafId = requestAnimationFrame(() => { - const { newSize, newPosition } = calculateNewSizeAndPosition( - position, - { width: initialWidth, height: initialHeight }, - initialPosition, - e.clientX - initialX, - e.clientY - initialY, + const handleMouseUp = () => { + if (rafId) { + cancelAnimationFrame(rafId); + rafId = null; + } + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + + const { dimensions, corner } = signalWidget.value; + const windowDims = getWindowDimensions(); + const isCurrentFullWidth = windowDims.isFullWidth(dimensions.width); + const isCurrentFullHeight = windowDims.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)`; - containerStyle.width = `${newSize.width}px`; - containerStyle.height = `${newSize.height}px`; signalWidget.value = { - ...signalWidget.value, + corner: newCorner, dimensions: { - isFullWidth: false, - isFullHeight: false, - width: newSize.width, - height: newSize.height, + 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, }, }; - rafId = null; + saveLocalStorage(LOCALSTORAGE_KEY, { + corner: newCorner, + dimensions: signalWidget.value.dimensions, + lastDimensions: signalWidget.value.lastDimensions, + }); + }; + + document.addEventListener('mousemove', handleMouseMove, { + passive: true, }); - }; + document.addEventListener('mouseup', handleMouseUp); + }, + [], + ); - const handleMouseUp = () => { - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + const handleDoubleClick = useCallback( + (e: JSX.TargetedMouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const container = signalRefContainer.value; + if (!container) return; + + const containerStyle = container.style; const { dimensions, corner } = signalWidget.value; - const { isFullWidth, isFullHeight } = getWindowDimensions(); - const isCurrentFullWidth = isFullWidth(dimensions.width); - const isCurrentFullHeight = isFullHeight(dimensions.height); + const windowDims = getWindowDimensions(); + + const isCurrentFullWidth = windowDims.isFullWidth(dimensions.width); + const isCurrentFullHeight = windowDims.isFullHeight(dimensions.height); const isFullScreen = isCurrentFullWidth && isCurrentFullHeight; + const isPartiallyMaximized = + (isCurrentFullWidth || isCurrentFullHeight) && !isFullScreen; + + let newWidth = dimensions.width; + let newHeight = dimensions.height; + const newCorner = getOppositeCorner( + position, + corner, + isFullScreen, + isCurrentFullWidth, + isCurrentFullHeight, + ); - let newCorner = corner; - if (isFullScreen || isCurrentFullWidth || isCurrentFullHeight) { - newCorner = getClosestCorner(dimensions.position); + if (position === 'left' || position === 'right') { + newWidth = isCurrentFullWidth ? dimensions.width : windowDims.maxWidth; + if (isPartiallyMaximized) { + newWidth = isCurrentFullWidth ? MIN_SIZE.width : windowDims.maxWidth; + } + } else { + newHeight = isCurrentFullHeight + ? dimensions.height + : windowDims.maxHeight; + if (isPartiallyMaximized) { + newHeight = isCurrentFullHeight + ? MIN_SIZE.height * 5 + : windowDims.maxHeight; + } } - const newPosition = calculatePosition( - newCorner, - dimensions.width, - dimensions.height, - ); + if (isFullScreen) { + if (position === 'left' || position === 'right') { + newWidth = MIN_SIZE.width; + } else { + newHeight = MIN_SIZE.height * 5; + } + } - const onTransitionEnd = () => { - container.removeEventListener('transitionend', onTransitionEnd); + const newPosition = calculatePosition(newCorner, newWidth, newHeight); + const newDimensions = { + isFullWidth: windowDims.isFullWidth(newWidth), + isFullHeight: windowDims.isFullHeight(newHeight), + width: newWidth, + height: newHeight, + position: newPosition, }; - 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, - }, - }; + requestAnimationFrame(() => { + signalWidget.value = { + corner: newCorner, + dimensions: newDimensions, + lastDimensions: dimensions, + }; - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: newCorner, - dimensions: signalWidget.value.dimensions, - lastDimensions: signalWidget.value.lastDimensions, + 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)`; }); - }; - - document.addEventListener('mousemove', handleMouseMove, { - passive: true, - }); - document.addEventListener('mouseup', handleMouseUp); - }, []); - - const handleDoubleClick = useCallback((e: JSX.TargetedMouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const container = signalRefContainer.value; - if (!container) return; - - const containerStyle = container.style; - const { dimensions, corner } = signalWidget.value; - const { maxWidth, maxHeight, isFullWidth, isFullHeight } = - getWindowDimensions(); - - const isCurrentFullWidth = isFullWidth(dimensions.width); - const isCurrentFullHeight = isFullHeight(dimensions.height); - const isFullScreen = isCurrentFullWidth && isCurrentFullHeight; - const isPartiallyMaximized = - (isCurrentFullWidth || isCurrentFullHeight) && !isFullScreen; - - let newWidth = dimensions.width; - let newHeight = dimensions.height; - const newCorner = getOppositeCorner( - position, - corner, - isFullScreen, - isCurrentFullWidth, - isCurrentFullHeight, - ); - 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; - } - } - - if (isFullScreen) { - if (position === 'left' || position === 'right') { - newWidth = MIN_SIZE.width; - } else { - 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, - }; - - requestAnimationFrame(() => { - signalWidget.value = { + saveLocalStorage(LOCALSTORAGE_KEY, { 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 (
{ 'resize-right peer/right': position === 'right', 'resize-top peer/top': position === 'top', 'resize-bottom peer/bottom': position === 'bottom', - } + }, )} > - + diff --git a/packages/scan/src/web/components/widget/toolbar/search.tsx b/packages/scan/src/web/components/widget/toolbar/search.tsx index 3edbb8f8..d05f6484 100644 --- a/packages/scan/src/web/components/widget/toolbar/search.tsx +++ b/packages/scan/src/web/components/widget/toolbar/search.tsx @@ -1,9 +1,14 @@ // TODO: @pivanov - improve UI and finish the implementation -import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; -import { Store } from "~core/index"; -import { getInspectableElements } from "~web/components/inspector/utils"; -import { cn } from "~web/utils/helpers"; - +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'preact/hooks'; +import { Store } from '~core/index'; +import { getInspectableElements } from '~web/components/inspector/utils'; +import { cn } from '~web/utils/helpers'; export const Search = () => { const [search, setSearch] = useState(''); @@ -43,15 +48,16 @@ export const Search = () => { const elements = useMemo(() => getInspectableElements(), []); // Get current focused element - const currentElement = Store.inspectState.value.kind === 'focused' - ? Store.inspectState.value.focusedDomElement - : null; + const currentElement = + Store.inspectState.value.kind === 'focused' + ? Store.inspectState.value.focusedDomElement + : null; const filteredElements = useMemo(() => { if (!search) return elements; const searchLower = search.toLowerCase(); - return elements.filter(item => - item.name.toLowerCase().includes(searchLower) + return elements.filter((item) => + item.name.toLowerCase().includes(searchLower), ); }, [elements, search]); @@ -61,7 +67,9 @@ export const Search = () => { // Find and select current element in the list if (currentElement) { - const index = filteredElements.findIndex(item => item.element === currentElement); + const index = filteredElements.findIndex( + (item) => item.element === currentElement, + ); if (index !== -1) { setSelectedIndex(index); // Scroll the item into view @@ -80,13 +88,11 @@ export const Search = () => { switch (e.key) { case 'ArrowDown': e.preventDefault(); - setSelectedIndex(i => - i < filteredElements.length - 1 ? i + 1 : i - ); + setSelectedIndex((i) => (i < filteredElements.length - 1 ? i + 1 : i)); break; case 'ArrowUp': e.preventDefault(); - setSelectedIndex(i => i > 0 ? i - 1 : i); + setSelectedIndex((i) => (i > 0 ? i - 1 : i)); break; case 'Enter': e.preventDefault(); @@ -110,7 +116,7 @@ export const Search = () => { ref={inputRef} type="text" value={search} - onChange={e => { + onChange={(e) => { setSearch(e.currentTarget.value); setSelectedIndex(0); }} @@ -130,10 +136,10 @@ export const Search = () => { }} className={cn( 'flex items-center px-2 py-1 cursor-pointer hover:bg-white/5', - selectedIndex === index && 'bg-white/10' + selectedIndex === index && 'bg-white/10', )} style={{ - paddingLeft: `${item.depth * 16 + 8}px` + paddingLeft: `${item.depth * 16 + 8}px`, }} > diff --git a/packages/scan/src/web/constants.ts b/packages/scan/src/web/constants.ts index 01717a40..07c83175 100644 --- a/packages/scan/src/web/constants.ts +++ b/packages/scan/src/web/constants.ts @@ -1,7 +1,7 @@ export const SAFE_AREA = 24; export const MIN_SIZE = { width: 360, - height: 36 + height: 36, } as const; export const LOCALSTORAGE_KEY = 'react-scan-widget-settings'; diff --git a/packages/scan/src/web/state.ts b/packages/scan/src/web/state.ts index c932fa7c..e556ab7a 100644 --- a/packages/scan/src/web/state.ts +++ b/packages/scan/src/web/state.ts @@ -1,7 +1,11 @@ import { signal } from '@preact/signals'; +import { + type Corner, + type WidgetConfig, + type WidgetSettings, +} from './components/widget/types'; +import { LOCALSTORAGE_KEY, MIN_SIZE, SAFE_AREA } from './constants'; import { readLocalStorage, saveLocalStorage } from './utils/helpers'; -import { MIN_SIZE, SAFE_AREA, LOCALSTORAGE_KEY } from './constants'; -import { type Corner, type WidgetConfig, type WidgetSettings } from './components/widget/types'; export const signalRefContainer = signal(null); @@ -12,15 +16,15 @@ export const defaultWidgetConfig = { isFullHeight: false, width: MIN_SIZE.width, height: MIN_SIZE.height, - position: { x: SAFE_AREA, y: SAFE_AREA } + position: { x: SAFE_AREA, y: SAFE_AREA }, }, lastDimensions: { isFullWidth: false, isFullHeight: false, width: MIN_SIZE.width, height: MIN_SIZE.height, - position: { x: SAFE_AREA, y: SAFE_AREA } - } + position: { x: SAFE_AREA, y: SAFE_AREA }, + }, } as WidgetConfig; export const getInitialWidgetConfig = (): WidgetConfig => { @@ -29,7 +33,7 @@ export const getInitialWidgetConfig = (): WidgetConfig => { saveLocalStorage(LOCALSTORAGE_KEY, { corner: defaultWidgetConfig.corner, dimensions: defaultWidgetConfig.dimensions, - lastDimensions: defaultWidgetConfig.lastDimensions + lastDimensions: defaultWidgetConfig.lastDimensions, }); return defaultWidgetConfig; @@ -42,9 +46,9 @@ export const getInitialWidgetConfig = (): WidgetConfig => { isFullHeight: false, width: MIN_SIZE.width, height: MIN_SIZE.height, - position: stored.dimensions.position + position: stored.dimensions.position, }, - lastDimensions: stored.dimensions + lastDimensions: stored.dimensions, }; }; @@ -59,11 +63,11 @@ export const updateDimensions = (): void => { signalWidget.value = { ...signalWidget.value, dimensions: { - isFullWidth: width >= window.innerWidth - (SAFE_AREA * 2), - isFullHeight: height >= window.innerHeight - (SAFE_AREA * 2), + isFullWidth: width >= window.innerWidth - SAFE_AREA * 2, + isFullHeight: height >= window.innerHeight - SAFE_AREA * 2, width, height, - position - } + position, + }, }; }; diff --git a/packages/scan/src/web/utils/geiger.ts b/packages/scan/src/web/utils/geiger.ts index e2a9668a..c7d2c8bc 100644 --- a/packages/scan/src/web/utils/geiger.ts +++ b/packages/scan/src/web/utils/geiger.ts @@ -1,7 +1,7 @@ // MIT License // Copyright (c) 2024 Kristian Dupont -import { isFirefox, readLocalStorage } from "./helpers"; +import { isFirefox, readLocalStorage } from './helpers'; // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -33,7 +33,10 @@ const FREQ_MULTIPLIER = 200; const DEFAULT_VOLUME = 0.5; // Ensure volume is between 0 and 1 -const storedVolume = Math.max(0, Math.min(1, readLocalStorage('react-scan-volume') ?? DEFAULT_VOLUME)); +const storedVolume = Math.max( + 0, + Math.min(1, readLocalStorage('react-scan-volume') ?? DEFAULT_VOLUME), +); // Audio configurations for different browsers const config = { @@ -52,7 +55,7 @@ const config = { endFreq: 220, attack: 0.0005, volumeMultiplier: storedVolume, - } + }, } as const; // Make entire config readonly // Cache the selected config @@ -66,7 +69,6 @@ export const playGeigerClickSound = ( audioContext: AudioContext, amplitude: number, ) => { - const now = performance.now(); if (now - lastPlayTime < MIN_INTERVAL) { return; @@ -78,26 +80,28 @@ export const playGeigerClickSound = ( const { duration, oscillatorType, startFreq, endFreq, attack } = audioConfig; // Pre-calculate volume once - const volume = Math.max(BASE_VOLUME, amplitude) * audioConfig.volumeMultiplier; + const volume = + Math.max(BASE_VOLUME, amplitude) * audioConfig.volumeMultiplier; // Create and configure nodes in one go const oscillator = new OscillatorNode(audioContext, { type: oscillatorType, - frequency: startFreq + amplitude * FREQ_MULTIPLIER + frequency: startFreq + amplitude * FREQ_MULTIPLIER, }); const gainNode = new GainNode(audioContext, { - gain: 0 + gain: 0, }); // Schedule all parameters - oscillator.frequency.exponentialRampToValueAtTime(endFreq, currentTime + duration); + oscillator.frequency.exponentialRampToValueAtTime( + endFreq, + currentTime + duration, + ); gainNode.gain.linearRampToValueAtTime(volume, currentTime + attack); // Connect and schedule playback - oscillator - .connect(gainNode) - .connect(audioContext.destination); + oscillator.connect(gainNode).connect(audioContext.destination); oscillator.start(currentTime); oscillator.stop(currentTime + duration); diff --git a/packages/scan/src/web/utils/outline-worker.ts b/packages/scan/src/web/utils/outline-worker.ts index 2878b301..7e5895ee 100644 --- a/packages/scan/src/web/utils/outline-worker.ts +++ b/packages/scan/src/web/utils/outline-worker.ts @@ -1,4 +1,4 @@ -import { SmolWorker } from "~core/worker/smol"; +import { SmolWorker } from '~core/worker/smol'; export interface DrawingQueue { rect: DOMRect; @@ -22,21 +22,21 @@ export interface SerializedOutlineLabel { export type OutlineWorkerAction = | { type: 'set-canvas'; payload: OffscreenCanvas } | { - type: 'fade-out-outline'; - payload: { - dpi: number; - drawingQueue: Array; - mergedLabels: Array; - }; - } + type: 'fade-out-outline'; + payload: { + dpi: number; + drawingQueue: Array; + mergedLabels: Array; + }; + } | { - type: 'resize'; - payload: { - width: number; - height: number; - dpi: number; + type: 'resize'; + payload: { + width: number; + height: number; + dpi: number; + }; }; - }; function setupOutlineWorker(): (action: OutlineWorkerAction) => Promise { const MONO_FONT = diff --git a/packages/scan/src/web/utils/preact/constant.ts b/packages/scan/src/web/utils/preact/constant.ts index 5fd62101..e57c4939 100644 --- a/packages/scan/src/web/utils/preact/constant.ts +++ b/packages/scan/src/web/utils/preact/constant.ts @@ -16,4 +16,3 @@ export function constant

(Component: FunctionComponent

) { Memoed._forwarded = true; return Memoed; } -