From d3d15333608b5068577d8a0b735ab60c49fa5ade Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sun, 19 Jul 2026 04:22:28 -0700 Subject: [PATCH 1/2] refactor: migrate React Scan overlay to Solid Replace the Preact overlay and internal reactive layer with a Solid-native runtime, simplify module boundaries and build tooling, and add comprehensive Playwright coverage for the rebuilt interface. --- .gitignore | 2 + AGENTS.md | 95 +- e2e/coverage/capture.ts | 57 + e2e/coverage/config.ts | 16 + e2e/coverage/report.ts | 201 + e2e/coverage/setup.ts | 8 + e2e/coverage/teardown.ts | 34 + e2e/fixtures.ts | 25 + e2e/globals.d.ts | 32 + e2e/helpers.ts | 89 +- e2e/inspector.spec.ts | 33 +- e2e/notifications.spec.ts | 22 +- e2e/outlines.spec.ts | 33 +- e2e/overlay-comprehensive.spec.ts | 407 ++ e2e/overlay-inspector.spec.ts | 60 +- e2e/overlay-notifications.spec.ts | 57 +- e2e/overlay-toolbar.spec.ts | 110 +- e2e/overlay-widget.spec.ts | 65 +- e2e/toolbar.spec.ts | 26 +- .../src/examples/e2e-fixture/index.tsx | 70 +- kitchen-sink/vite.config.ts | 10 +- package.json | 4 +- packages/scan/package.json | 36 +- .../scan/scripts/finalize-declarations.mjs | 39 + packages/scan/solid-vite-plugin.ts | 137 + packages/scan/src/auto.ts | 10 +- packages/scan/src/cli-utils.mts | 244 +- packages/scan/src/cli-utils.test.mts | 600 ++- packages/scan/src/cli.mts | 125 +- packages/scan/src/core/all-environments.ts | 4 +- packages/scan/src/core/compatibility.ts | 48 + packages/scan/src/core/fast-serialize.test.ts | 64 +- .../scan/src/core/get-is-production.test.ts | 20 +- packages/scan/src/core/index.test.ts | 64 + packages/scan/src/core/index.ts | 173 +- .../inspection/change-collection.ts} | 210 +- packages/scan/src/core/inspection/fiber.ts | 94 + packages/scan/src/core/inspection/types.ts | 22 + packages/scan/src/core/instrumentation.ts | 53 +- packages/scan/src/core/native-state.ts | 42 + .../src/core/notifications/event-tracking.ts | 214 +- .../core/notifications/interaction-store.ts | 36 - .../src/core/notifications/outline-overlay.ts | 29 +- .../core/notifications/performance-store.ts | 120 - .../src/core/notifications/performance.ts | 61 +- packages/scan/src/core/utils.ts | 8 +- packages/scan/src/index.ts | 6 +- packages/scan/src/install-hook.ts | 2 +- packages/scan/src/lite/change-description.ts | 19 +- packages/scan/src/lite/constants.ts | 14 +- packages/scan/src/lite/create-emitter.ts | 10 +- .../scan/src/lite/create-profiling-hooks.ts | 62 +- packages/scan/src/lite/fiber-source.ts | 33 +- packages/scan/src/lite/lane-labels.ts | 6 +- packages/scan/src/lite/lite.test.ts | 368 +- packages/scan/src/lite/types.ts | 68 +- packages/scan/src/lite/walk-fiber.ts | 19 +- packages/scan/src/new-outlines/index.ts | 57 +- .../new-outlines/offscreen-canvas.worker.ts | 12 +- packages/scan/src/polyfills.ts | 4 +- .../__tests__/arrow-function.test.ts | 18 +- .../__tests__/complex-patterns.test.ts | 14 +- .../__tests__/function-declarations.test.ts | 10 +- .../__tests__/general-cases.test.ts | 102 +- .../__tests__/react-patterns.test.ts | 14 +- .../__tests__/ts-patterns.test.ts | 10 +- .../react-component-name/__tests__/utils.ts | 11 +- .../scan/src/react-component-name/astro.ts | 8 +- .../src/react-component-name/babel/index.ts | 98 +- .../babel/is-componentish-name.ts | 12 +- .../babel/is-nested-expression.ts | 16 +- .../babel/is-path-valid.ts | 4 +- .../babel/path-references-import.ts | 31 +- .../src/react-component-name/babel/unwrap.ts | 10 +- .../scan/src/react-component-name/esbuild.ts | 4 +- .../scan/src/react-component-name/index.ts | 68 +- .../scan/src/react-component-name/loader.ts | 27 +- .../scan/src/react-component-name/rolldown.ts | 1 - .../scan/src/react-component-name/rollup.ts | 2 +- .../scan/src/react-component-name/rspack.ts | 2 +- .../src/react-component-name/tsconfig.json | 2 +- .../scan/src/react-component-name/vite.ts | 2 +- .../scan/src/react-component-name/webpack.ts | 2 +- packages/scan/src/rsc-shim.ts | 14 + .../scan/src/runtime/create-scan-runtime.ts | 66 + packages/scan/src/runtime/root-container.ts | 28 + packages/scan/src/types.ts | 3 + .../utils/check-react-grab-version.ts | 0 packages/scan/src/utils/is-client.ts | 1 + .../{web => }/utils/is-finite-non-negative.ts | 2 +- packages/scan/src/utils/is-plain-object.ts | 2 + .../utils/parse-safe-area-option.test.ts | 45 +- .../{web => }/utils/parse-safe-area-option.ts | 19 +- packages/scan/src/utils/read-local-storage.ts | 12 + packages/scan/src/utils/save-local-storage.ts | 9 + .../src/web/assets/css/styles.tailwind.css | 141 +- .../components/copy-to-clipboard/index.tsx | 106 +- .../scan/src/web/components/icon/index.tsx | 57 +- .../src/web/components/svg-sprite/index.tsx | 164 +- .../scan/src/web/components/toggle/index.tsx | 24 +- .../scan/src/web/components/tooltip/index.tsx | 74 + packages/scan/src/web/constants.ts | 27 +- .../scan/src/web/hooks/use-delayed-value.ts | 31 +- .../scan/src/web/hooks/use-virtual-list.ts | 117 - packages/scan/src/web/state.ts | 64 +- packages/scan/src/web/toolbar.tsx | 120 +- packages/scan/src/web/utils/clamp-to-range.ts | 2 + packages/scan/src/web/utils/constants.ts | 1 - packages/scan/src/web/utils/create-store.ts | 138 - .../scan/src/web/utils/create-toolbar-drag.ts | 161 + .../src/web/utils/get-corner-from-edge.ts | 11 + .../scan/src/web/utils/get-visual-viewport.ts | 25 + packages/scan/src/web/utils/helpers.ts | 30 +- .../scan/src/web/utils/is-plain-object.ts | 4 - packages/scan/src/web/utils/log.ts | 12 +- packages/scan/src/web/utils/pin.ts | 13 +- .../scan/src/web/utils/preact/constant.ts | 23 - packages/scan/src/web/utils/safe-area.ts | 14 +- .../scan/src/web/utils/toolbar-position.ts | 177 + .../scan/src/web/utils/watch-app-theme.ts | 80 + packages/scan/src/web/views/index.tsx | 137 +- .../views/inspector/components-tree/index.tsx | 1292 ++++--- .../views/inspector/components-tree/state.ts | 22 +- .../inspector/components-tree/virtual-list.ts | 98 + .../src/web/views/inspector/diff-value.tsx | 345 +- .../src/web/views/inspector/flash-overlay.ts | 22 +- .../scan/src/web/views/inspector/header.tsx | 138 +- .../scan/src/web/views/inspector/index.tsx | 239 +- .../src/web/views/inspector/overlay/index.tsx | 337 +- .../scan/src/web/views/inspector/states.ts | 60 +- .../scan/src/web/views/inspector/utils.ts | 959 +---- .../src/web/views/inspector/what-changed.tsx | 1047 +++--- .../whats-changed/use-change-store.ts | 112 +- .../views/notifications/collapsed-event.tsx | 242 +- .../scan/src/web/views/notifications/data.ts | 31 +- .../views/notifications/details-routes.tsx | 311 +- .../src/web/views/notifications/icons.tsx | 193 +- .../notifications/notification-header.tsx | 187 +- .../views/notifications/notification-tabs.tsx | 134 +- .../web/views/notifications/notifications.tsx | 569 ++- .../src/web/views/notifications/optimize.tsx | 517 --- .../notifications/other-visualization.tsx | 757 ++-- .../src/web/views/notifications/popover.tsx | 254 +- .../views/notifications/render-bar-chart.tsx | 316 +- .../notifications/render-explanation.tsx | 292 +- .../views/notifications/slowdown-history.tsx | 581 ++- packages/scan/src/web/views/toolbar/index.tsx | 344 +- .../src/web/widget/collapsed-indicator.tsx | 28 + packages/scan/src/web/widget/fps-meter.tsx | 41 +- packages/scan/src/web/widget/header.tsx | 122 +- packages/scan/src/web/widget/helpers.ts | 230 +- packages/scan/src/web/widget/index.tsx | 980 ++--- .../scan/src/web/widget/resize-handle.tsx | 579 ++- packages/scan/src/web/widget/types.ts | 10 +- packages/scan/tsconfig.json | 11 +- packages/scan/tsup.config.ts | 246 -- packages/scan/vite.config.mts | 278 +- packages/scan/worker-plugin.ts | 33 - packages/website/public/auto.global.js | 3241 +++++++++++++++- playwright.config.ts | 26 +- pnpm-lock.yaml | 3261 +++-------------- 161 files changed, 12649 insertions(+), 12847 deletions(-) create mode 100644 e2e/coverage/capture.ts create mode 100644 e2e/coverage/config.ts create mode 100644 e2e/coverage/report.ts create mode 100644 e2e/coverage/setup.ts create mode 100644 e2e/coverage/teardown.ts create mode 100644 e2e/fixtures.ts create mode 100644 e2e/globals.d.ts create mode 100644 e2e/overlay-comprehensive.spec.ts create mode 100644 packages/scan/scripts/finalize-declarations.mjs create mode 100644 packages/scan/solid-vite-plugin.ts create mode 100644 packages/scan/src/core/compatibility.ts create mode 100644 packages/scan/src/core/index.test.ts rename packages/scan/src/{web/views/inspector/timeline/utils.ts => core/inspection/change-collection.ts} (82%) create mode 100644 packages/scan/src/core/inspection/fiber.ts create mode 100644 packages/scan/src/core/inspection/types.ts create mode 100644 packages/scan/src/core/native-state.ts delete mode 100644 packages/scan/src/core/notifications/interaction-store.ts delete mode 100644 packages/scan/src/core/notifications/performance-store.ts create mode 100644 packages/scan/src/rsc-shim.ts create mode 100644 packages/scan/src/runtime/create-scan-runtime.ts create mode 100644 packages/scan/src/runtime/root-container.ts rename packages/scan/src/{web => }/utils/check-react-grab-version.ts (100%) create mode 100644 packages/scan/src/utils/is-client.ts rename packages/scan/src/{web => }/utils/is-finite-non-negative.ts (54%) create mode 100644 packages/scan/src/utils/is-plain-object.ts rename packages/scan/src/{web => }/utils/parse-safe-area-option.test.ts (53%) rename packages/scan/src/{web => }/utils/parse-safe-area-option.ts (69%) create mode 100644 packages/scan/src/utils/read-local-storage.ts create mode 100644 packages/scan/src/utils/save-local-storage.ts create mode 100644 packages/scan/src/web/components/tooltip/index.tsx delete mode 100644 packages/scan/src/web/hooks/use-virtual-list.ts create mode 100644 packages/scan/src/web/utils/clamp-to-range.ts delete mode 100644 packages/scan/src/web/utils/constants.ts delete mode 100644 packages/scan/src/web/utils/create-store.ts create mode 100644 packages/scan/src/web/utils/create-toolbar-drag.ts create mode 100644 packages/scan/src/web/utils/get-corner-from-edge.ts create mode 100644 packages/scan/src/web/utils/get-visual-viewport.ts delete mode 100644 packages/scan/src/web/utils/is-plain-object.ts delete mode 100644 packages/scan/src/web/utils/preact/constant.ts create mode 100644 packages/scan/src/web/utils/toolbar-position.ts create mode 100644 packages/scan/src/web/utils/watch-app-theme.ts create mode 100644 packages/scan/src/web/views/inspector/components-tree/virtual-list.ts delete mode 100644 packages/scan/src/web/views/notifications/optimize.tsx create mode 100644 packages/scan/src/web/widget/collapsed-indicator.tsx delete mode 100644 packages/scan/tsup.config.ts delete mode 100644 packages/scan/worker-plugin.ts diff --git a/.gitignore b/.gitignore index cb567e7b..90800259 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ playgrounds # Playwright test-results/ playwright-report/ +/.coverage-v8/ +/coverage/ .cursor diff --git a/AGENTS.md b/AGENTS.md index 16fdc5c3..2673ae56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,45 +17,74 @@ - MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`). - MUST: Put small, focused utility functions in `utils/` with one utility per file. - MUST: Use `Boolean(x)` over `!!x`. +- MUST: Avoid dynamic imports unless they are required for meaningful code splitting or an otherwise unavoidable cycle. -## Preact + @preact/signals Rules +## V8 Hot-Path Rules -React Scan's overlay UI runs as a Preact app mounted inside a Shadow DOM (see [`packages/scan/src/core/index.ts`](packages/scan/src/core/index.ts)). Reactivity is driven by `@preact/signals`, not React state. +For hot per-frame paths such as pointer handlers, animation ticks, outline drawing, and fiber walks: -### Signals +- MUST: Keep indirect call sites monomorphic. Split hot iterators by callback shape or inline the loop. +- MUST: Mutate stable object fields in place instead of allocating replacement objects every frame. +- MUST: Keep numeric helpers in one number lane. Avoid mixing integer and double return shapes at the same hot call site. +- SHOULD: Prefer closed-form arithmetic over normalization loops. +- SHOULD: Reuse arrays, maps, rectangles, and coordinate records when profiling shows allocation pressure. -- MUST: Read signals via `.value` inside JSX/effects: `signalWidget.value` not `signalWidget()`. -- MUST: Write signals atomically: `signalWidget.value = { ...signalWidget.value, dimensions: ... }`. One signal per logical slice. -- MUST: Use `useSignal(initial)` for component-local reactive state, `useComputed(() => ...)` for derived values, `useSignalEffect(() => ...)` for subscriptions/DOM imperative work. -- MUST: Wrap module-level signals exported from a tree-shaken bundle with `/* @__PURE__ */ signal(...)` so production builds can drop unused state. -- SHOULD: Persist user-facing state via `readLocalStorage`/`saveLocalStorage` from [`~web/utils/helpers`](packages/scan/src/web/utils/helpers.ts) and seed the signal with the persisted value. -- NEVER: Mirror one signal into another inside `useSignalEffect` - find the single source of truth (compute on read). +## SolidJS Rules + +React Scan's overlay UI is a SolidJS app mounted inside the Shadow DOM created by [`packages/scan/src/core/index.ts`](packages/scan/src/core/index.ts). + +### Mental Model + +- MUST: Treat components as setup functions that run once. +- MUST: Put reactive work in Solid primitives and JSX control flow, not untracked component-body reads. +- MUST: Access signals only inside reactive contexts. + +### Reactivity + +- MUST: Read signals by calling their accessors. +- MUST: Use functional setters when the next value depends on the previous value. +- MUST: Keep signals focused and use stores for nested state that benefits from path updates. +- MUST: Use derived functions for cheap derivations and `createMemo` for expensive or frequently-read derivations. +- MUST: Use `createEffect` only for external side effects. +- MUST: Register listener, timer, observer, and subscription cleanup with `onCleanup`. +- SHOULD: Use `batch` when multiple writes outside an event handler form one logical update. +- SHOULD: Use `on` for explicit effect dependencies and `untrack` for intentional non-reactive reads. +- NEVER: Mirror one signal or store into another in an effect. +- NEVER: Put side effects inside `createMemo`. ### Effects -Before reaching for `useEffect`/`useSignalEffect`, classify the work: +Before reaching for `createEffect`, classify the work: -- MUST: Use `useComputed` when the result is pure derived state from other signals. If no external system is touched, it is not an effect. +- MUST: Use `createMemo` when the result is pure derived state. - MUST: Use event handlers and direct action calls when work happens because the user clicked, dragged, or navigated. Do not watch a flag in an effect to trigger imperative logic. -- MUST: Use `useEffect(..., [])` (or `useSignalEffect` with no deps) for one-time mount/cleanup of subscriptions, timers, listeners, and `MutationObserver`s. Always return the cleanup. +- MUST: Use `onMount` and `onCleanup` for one-time lifecycle setup and teardown. - MUST: Keep each effect single-purpose - one effect, one external bridge. Split mixed-responsibility effects. - NEVER: Use an effect just to copy one signal into another. - NEVER: Use an effect as an event bus (watching a trigger signal to run a command). Call the action directly from the event source. ### Props -- MUST: Access props via `props.title`, not destructuring, when the prop is read inside an event handler that fires later or inside an effect. -- SHOULD: Destructure props at the top of the body for purely-rendered values (read once during render). -- NEVER: Destructure a prop that is itself a signal-driven slice if you intend to re-read it after async work. +- MUST: Access reactive props through `props.title`; do not destructure them. +- SHOULD: Use `splitProps` for local versus pass-through props and `mergeProps` for defaults. +- SHOULD: Use a getter when a derived prop name improves readability. + +### Control Flow + +- MUST: Use `` for object arrays and `` for primitive arrays or editable items. +- SHOULD: Use `` for conditional branches and `/` for multiple exclusive branches. +- NEVER: Use `.map()` directly in JSX. ### JSX & DOM -- MUST: Use `className` (we render via Preact's React-compat JSX in [`tsconfig.json`](tsconfig.json) `jsxImportSource: preact`). -- MUST: Combine static `className="btn"` with reactive `className={cn("btn", isActive.value && "active")}` via [`cn`](packages/scan/src/web/utils/helpers.ts). -- MUST: Read refs in `useEffect` or via the callback ref pattern - DOM refs are populated after render. +- MUST: Use `class`, not `className`. +- MUST: Use `classList` for reactive class toggles and `cn` for composed static classes. +- MUST: Read refs after mount or inside reactive callbacks where connection is guaranteed. - MUST: Mount overlay UI under the existing shadow root from `initRootContainer()` in [`core/index.ts`](packages/scan/src/core/index.ts). Do not append directly to `document.body`. -- SHOULD: Use `style={{ "--css-var": value }}` for dynamic CSS variables; class toggles for boolean states. -- SHOULD: Type refs as `let element: HTMLElement | null = null` with a guard. +- SHOULD: Use CSS variables for dynamic scalar styles and classes for boolean visual state. +- SHOULD: Type refs as `let element: HTMLElement | undefined` and guard before use. +- SHOULD: Use native `on:event` listeners when propagation, capture, passive options, or exact element ownership matters. +- NEVER: Mix a reactive `class` expression with `classList` on the same element. ## Build & Toolchain @@ -75,19 +104,19 @@ The root [`package.json`](package.json) declares `pnpm.onlyBuiltDependencies` fo ### Key commands -| Task | Command | -| -------------- | -------------------------------------------- | -| Install | `pnpm install` | -| Build | `pnpm build` | -| Dev watch | `pnpm dev` (watches `react-scan` + kitchen-sink) | -| Unit tests | `pnpm test` | -| E2E tests | `pnpm test:e2e` | -| Lint | `pnpm lint` (oxlint via vite-plus) | -| Lint + fix | `pnpm lint:fix` | -| Format | `pnpm format` (oxfmt via vite-plus) | -| Format check | `pnpm format:check` | -| Typecheck | `pnpm typecheck` | -| Combined | `pnpm check` (lint + fmt check + typecheck) | +| Task | Command | +| ------------ | ------------------------------------------------ | +| Install | `pnpm install` | +| Build | `pnpm build` | +| Dev watch | `pnpm dev` (watches `react-scan` + kitchen-sink) | +| Unit tests | `pnpm test` | +| E2E tests | `pnpm test:e2e` | +| Lint | `pnpm lint` (oxlint via vite-plus) | +| Lint + fix | `pnpm lint:fix` | +| Format | `pnpm format` (oxfmt via vite-plus) | +| Format check | `pnpm format:check` | +| Typecheck | `pnpm typecheck` | +| Combined | `pnpm check` (lint + fmt check + typecheck) | ## Testing diff --git a/e2e/coverage/capture.ts b/e2e/coverage/capture.ts new file mode 100644 index 00000000..4cecec81 --- /dev/null +++ b/e2e/coverage/capture.ts @@ -0,0 +1,57 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { Page } from "@playwright/test"; + +export interface V8CoverageEntry { + url: string; + source?: string; + scriptId?: string; + functions?: unknown[]; +} + +export const cleanRawCoverage = (rawDirectory: string): void => { + rmSync(rawDirectory, { recursive: true, force: true }); + mkdirSync(rawDirectory, { recursive: true }); +}; + +export const writeRawCoverage = ( + rawDirectory: string, + coverageEntries: V8CoverageEntry[], +): void => { + if (coverageEntries.length === 0) return; + + try { + mkdirSync(rawDirectory, { recursive: true }); + writeFileSync(join(rawDirectory, `${randomUUID()}.json`), JSON.stringify(coverageEntries)); + } catch { + return; + } +}; + +export const captureCoverage = async ( + page: Page, + rawDirectory: string, + use: () => Promise, +): Promise => { + let didStartCoverage = false; + + try { + await page.coverage.startJSCoverage({ resetOnNavigation: false }); + didStartCoverage = true; + } catch { + didStartCoverage = false; + } + + try { + await use(); + } finally { + if (didStartCoverage) { + try { + writeRawCoverage(rawDirectory, await page.coverage.stopJSCoverage()); + } catch { + // Coverage must not affect test results. + } + } + } +}; diff --git a/e2e/coverage/config.ts b/e2e/coverage/config.ts new file mode 100644 index 00000000..e05eb38c --- /dev/null +++ b/e2e/coverage/config.ts @@ -0,0 +1,16 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const E2E_DIRECTORY = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +export const REPOSITORY_ROOT = resolve(E2E_DIRECTORY, ".."); +export const COVERAGE_OUTPUT_DIRECTORY = resolve(REPOSITORY_ROOT, "coverage"); +export const COVERAGE_RAW_DIRECTORY = resolve(REPOSITORY_ROOT, ".coverage-v8"); + +export const isReactScanSource = (sourcePath: string): boolean => { + const normalizedSourcePath = sourcePath.replaceAll("\\", "/"); + return ( + !normalizedSourcePath.includes("/node_modules/") && + normalizedSourcePath.includes("packages/scan/src/") + ); +}; diff --git a/e2e/coverage/report.ts b/e2e/coverage/report.ts new file mode 100644 index 00000000..dedfe9ec --- /dev/null +++ b/e2e/coverage/report.ts @@ -0,0 +1,201 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { dirname, isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { V8CoverageEntry } from "./capture"; + +const SOURCE_MAP_PATTERN = /\/\/[#@]\s*sourceMappingURL=(\S+)/; +const DEFAULT_REPORTS = ["v8", "console-details", "lcovonly"]; + +interface ScriptCoverage { + functions: unknown[]; +} + +interface MergeV8Coverage { + (scriptCoverages: ScriptCoverage[]): ScriptCoverage; +} + +interface MonocartUtility { + mergeV8Coverage: MergeV8Coverage; +} + +interface CoverageAccumulator { + source: string; + mergedCoverage: ScriptCoverage; +} + +interface SourceMapData { + sources?: string[]; + sourceRoot?: string; +} + +export interface CoverageSummary { + fileCount: number; + coveredLines: number; + totalLines: number; + percentageLines: number; +} + +export interface GenerateCoverageReportOptions { + rawDirectory: string; + outputDirectory: string; + baseDirectory: string; + name: string; + sourceFilter: (sourcePath: string) => boolean; +} + +const urlToLocalPath = (url: string): string | null => { + if (url.startsWith("file://")) { + try { + return fileURLToPath(url); + } catch { + return null; + } + } + + try { + const pathname = new URL(url).pathname; + const fileSystemMarker = "/@fs"; + const fileSystemMarkerIndex = pathname.indexOf(`${fileSystemMarker}/`); + + if (fileSystemMarkerIndex !== -1) { + return decodeURIComponent(pathname.slice(fileSystemMarkerIndex + fileSystemMarker.length)); + } + } catch { + return isAbsolute(url) && existsSync(url) ? url : null; + } + + if (isAbsolute(url) && existsSync(url)) return url; + return null; +}; + +const inlineSourceMap = (source: string, sourceMap: string): string => { + const sourceMapDataUrl = `data:application/json;base64,${Buffer.from(sourceMap).toString("base64")}`; + + if (SOURCE_MAP_PATTERN.test(source)) { + return source.replace(SOURCE_MAP_PATTERN, `//# sourceMappingURL=${sourceMapDataUrl}`); + } + + return `${source}\n//# sourceMappingURL=${sourceMapDataUrl}`; +}; + +const absolutizeSourceMap = (sourceMapJson: string, sourceMapPath: string): string => { + const sourceMap = JSON.parse(sourceMapJson) as SourceMapData; + const sourceMapDirectory = dirname(sourceMapPath); + const sourceRoot = sourceMap.sourceRoot ?? ""; + + sourceMap.sources = (sourceMap.sources ?? []).map((sourcePath) => + isAbsolute(sourcePath) ? sourcePath : resolve(sourceMapDirectory, sourceRoot, sourcePath), + ); + sourceMap.sourceRoot = ""; + + return JSON.stringify(sourceMap); +}; + +const mergeRawCoverage = ( + rawDirectory: string, + mergeV8Coverage: MergeV8Coverage, +): Map => { + const coverageByPath = new Map(); + let rawCoverageFiles: string[]; + + try { + rawCoverageFiles = readdirSync(rawDirectory); + } catch { + return coverageByPath; + } + + for (const rawCoverageFile of rawCoverageFiles) { + if (!rawCoverageFile.endsWith(".json")) continue; + + let coverageEntries: V8CoverageEntry[]; + try { + coverageEntries = JSON.parse(readFileSync(join(rawDirectory, rawCoverageFile), "utf8")); + } catch { + continue; + } + + if (!Array.isArray(coverageEntries)) continue; + + for (const coverageEntry of coverageEntries) { + if (!coverageEntry.url || typeof coverageEntry.source !== "string") { + continue; + } + + const localPath = urlToLocalPath(coverageEntry.url); + if (!localPath || !existsSync(`${localPath}.map`)) continue; + + const scriptCoverage: ScriptCoverage = { + functions: coverageEntry.functions ?? [], + }; + const accumulator = coverageByPath.get(localPath); + + if (accumulator) { + accumulator.mergedCoverage = mergeV8Coverage([accumulator.mergedCoverage, scriptCoverage]); + } else { + coverageByPath.set(localPath, { + source: coverageEntry.source, + mergedCoverage: scriptCoverage, + }); + } + } + } + + return coverageByPath; +}; + +const createReportEntries = ( + coverageByPath: Map, +): V8CoverageEntry[] => { + const reportEntries: V8CoverageEntry[] = []; + + for (const [localPath, accumulator] of coverageByPath) { + const sourceMapPath = `${localPath}.map`; + + try { + const sourceMap = absolutizeSourceMap(readFileSync(sourceMapPath, "utf8"), sourceMapPath); + reportEntries.push({ + url: localPath, + source: inlineSourceMap(accumulator.source, sourceMap), + scriptId: "0", + functions: accumulator.mergedCoverage.functions, + }); + } catch { + continue; + } + } + + return reportEntries; +}; + +export const generateCoverageReport = async ( + options: GenerateCoverageReportOptions, +): Promise => { + const { default: createCoverageReport } = await import("monocart-coverage-reports"); + const monocartUtility = createCoverageReport.Util as unknown as MonocartUtility; + const coverageByPath = mergeRawCoverage(options.rawDirectory, monocartUtility.mergeV8Coverage); + + if (coverageByPath.size === 0) return null; + + const coverageReport = createCoverageReport({ + name: options.name, + outputDir: options.outputDirectory, + baseDir: options.baseDirectory, + reports: DEFAULT_REPORTS, + cleanCache: true, + sourceFilter: options.sourceFilter, + }); + + await coverageReport.add(createReportEntries(coverageByPath)); + const results = await coverageReport.generate(); + if (!results) return null; + + const lineSummary = results.summary.lines; + const percentageLines = typeof lineSummary.pct === "number" ? lineSummary.pct : 0; + + return { + fileCount: results.files.length, + coveredLines: lineSummary.covered, + totalLines: lineSummary.total, + percentageLines, + }; +}; diff --git a/e2e/coverage/setup.ts b/e2e/coverage/setup.ts new file mode 100644 index 00000000..9ca3422b --- /dev/null +++ b/e2e/coverage/setup.ts @@ -0,0 +1,8 @@ +import { cleanRawCoverage } from "./capture"; +import { COVERAGE_RAW_DIRECTORY } from "./config"; + +const setupCoverage = async (): Promise => { + cleanRawCoverage(COVERAGE_RAW_DIRECTORY); +}; + +export default setupCoverage; diff --git a/e2e/coverage/teardown.ts b/e2e/coverage/teardown.ts new file mode 100644 index 00000000..04247ed7 --- /dev/null +++ b/e2e/coverage/teardown.ts @@ -0,0 +1,34 @@ +import { relative } from "node:path"; +import { + COVERAGE_OUTPUT_DIRECTORY, + COVERAGE_RAW_DIRECTORY, + isReactScanSource, + REPOSITORY_ROOT, +} from "./config"; +import { generateCoverageReport } from "./report"; + +const teardownCoverage = async (): Promise => { + try { + const summary = await generateCoverageReport({ + rawDirectory: COVERAGE_RAW_DIRECTORY, + outputDirectory: COVERAGE_OUTPUT_DIRECTORY, + baseDirectory: REPOSITORY_ROOT, + name: "React Scan coverage", + sourceFilter: isReactScanSource, + }); + + if (!summary) { + console.warn("No V8 coverage was captured."); + return; + } + + console.log( + `\nReact Scan line coverage: ${summary.percentageLines.toFixed(2)}% (${summary.coveredLines}/${summary.totalLines} lines across ${summary.fileCount} files)`, + ); + console.log(`Reports written to ${relative(process.cwd(), COVERAGE_OUTPUT_DIRECTORY)}`); + } catch (error) { + console.warn("Failed to generate coverage report:", error); + } +}; + +export default teardownCoverage; diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts new file mode 100644 index 00000000..7ca59dea --- /dev/null +++ b/e2e/fixtures.ts @@ -0,0 +1,25 @@ +import { expect, test as base } from "@playwright/test"; +import { captureCoverage } from "./coverage/capture"; +import { COVERAGE_RAW_DIRECTORY } from "./coverage/config"; + +const IS_COVERAGE_ENABLED = Boolean(process.env.COVERAGE); + +interface CoverageFixtures { + coverageCapture: void; +} + +export const test = base.extend({ + coverageCapture: [ + async ({ page }, use) => { + if (!IS_COVERAGE_ENABLED) { + await use(); + return; + } + + await captureCoverage(page, COVERAGE_RAW_DIRECTORY, use); + }, + { auto: true }, + ], +}); + +export { expect }; diff --git a/e2e/globals.d.ts b/e2e/globals.d.ts new file mode 100644 index 00000000..ca4c1bea --- /dev/null +++ b/e2e/globals.d.ts @@ -0,0 +1,32 @@ +interface ReactScanE2ESignal { + value: Value; +} + +interface ReactScanE2EOptions { + enabled?: boolean; + dangerouslyForceRunInProduction?: boolean; + showToolbar?: boolean; + useOffscreenCanvasWorker?: boolean; + onRender?: (...arguments_: Array) => void; +} + +interface ReactScanE2EInternals { + options?: ReactScanE2ESignal; + Store?: { + inspectState?: ReactScanE2ESignal<{ + kind?: "uninitialized" | "inspect-off" | "inspecting" | "focused"; + focusedDomElement?: unknown; + }>; + interactionListeningForRenders?: unknown; + }; + instrumentation?: { + isPaused?: ReactScanE2ESignal; + }; +} + +interface Window { + __E2E_RENDER_COUNT__?: number; + __REACT_SCAN__?: { + ReactScanInternals?: ReactScanE2EInternals; + }; +} diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 5226ef0c..c307a4d4 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -1,44 +1,41 @@ -import { type Locator, type Page } from '@playwright/test'; +import { type Locator, type Page } from "@playwright/test"; -export const FIXTURE_URL = '/?example=e2e-fixture'; +export const FIXTURE_URL = "/?example=e2e-fixture"; export const TOOLBAR_SELECTORS = { - root: '#react-scan-root', - widget: '#react-scan-toolbar', - inspectButton: '#react-scan-inspect-element', - notificationsButton: '#react-scan-notifications', + root: "#react-scan-root", + widget: "#react-scan-toolbar", + panel: "#react-scan-panel", + inspectButton: "#react-scan-inspect-element", + notificationsButton: "#react-scan-notifications", outlineToggle: '.react-scan-toggle input[type="checkbox"]', closeButton: '.react-scan-close-button[title="Close"]', - inspectorPanel: '.react-scan-inspector', - overlayCanvas: 'canvas.react-scan-inspector-overlay', + inspectorPanel: ".react-scan-inspector", + overlayCanvas: "canvas.react-scan-inspector-overlay", } as const; -export type InspectStateKind = - | 'uninitialized' - | 'inspect-off' - | 'inspecting' - | 'focused'; +export type InspectStateKind = "uninitialized" | "inspect-off" | "inspecting" | "focused"; -export async function gotoFixture(page: Page): Promise { - await page.goto(FIXTURE_URL); +export async function gotoFixture(page: Page, fixtureUrl = FIXTURE_URL): Promise { + await page.goto(fixtureUrl); await page.waitForSelector('[data-testid="heading"]', { timeout: 10_000 }); // Wait for React Scan to boot and expose __REACT_SCAN__ await page.waitForFunction( - () => typeof (window as any).__REACT_SCAN__?.ReactScanInternals !== 'undefined', + () => typeof window.__REACT_SCAN__?.ReactScanInternals !== "undefined", { timeout: 15_000 }, ); // Install a render counter by patching the onRender option on the signal await page.evaluate(() => { - (window as any).__E2E_RENDER_COUNT__ = 0; - const internals = (window as any).__REACT_SCAN__?.ReactScanInternals; + window.__E2E_RENDER_COUNT__ = 0; + const internals = window.__REACT_SCAN__?.ReactScanInternals; if (internals?.options) { const prev = internals.options.value; const prevOnRender = prev.onRender; internals.options.value = { ...prev, - onRender: (...args: any[]) => { - (window as any).__E2E_RENDER_COUNT__++; - if (prevOnRender) prevOnRender(...args); + onRender: (...arguments_: Array) => { + window.__E2E_RENDER_COUNT__ = (window.__E2E_RENDER_COUNT__ ?? 0) + 1; + prevOnRender?.(...arguments_); }, }; } @@ -46,24 +43,21 @@ export async function gotoFixture(page: Page): Promise { // Wait for initial mount renders to settle then reset await page.waitForTimeout(500); await page.evaluate(() => { - (window as any).__E2E_RENDER_COUNT__ = 0; + window.__E2E_RENDER_COUNT__ = 0; }); } export async function getRenderCount(page: Page): Promise { - return page.evaluate(() => (window as any).__E2E_RENDER_COUNT__ ?? 0); + return page.evaluate(() => window.__E2E_RENDER_COUNT__ ?? 0); } -export async function waitForRenders( - page: Page, - timeout = 5000, -): Promise { +export async function waitForRenders(page: Page, timeout = 5000): Promise { const startCount = await getRenderCount(page); return page.evaluate( ({ start, t }) => { return new Promise((resolve) => { const check = () => { - const current = (window as any).__E2E_RENDER_COUNT__ ?? 0; + const current = window.__E2E_RENDER_COUNT__ ?? 0; if (current > start) { resolve(current - start); return true; @@ -86,21 +80,19 @@ export async function waitForRenders( export async function isReactScanActive(page: Page): Promise { return page.evaluate(() => { - return typeof (window as any).__REACT_SCAN__ !== 'undefined'; + return typeof window.__REACT_SCAN__ !== "undefined"; }); } export async function hasShadowRoot(page: Page): Promise { return page.evaluate(() => { - return document.getElementById('react-scan-root')?.shadowRoot != null; + return document.getElementById("react-scan-root")?.shadowRoot != null; }); } -export async function getInspectStateKind( - page: Page, -): Promise { +export async function getInspectStateKind(page: Page): Promise { return page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; return scan?.ReactScanInternals?.Store?.inspectState?.value?.kind ?? null; }); } @@ -112,21 +104,17 @@ export async function waitForInspectStateKind( ): Promise { await page.waitForFunction( (kind) => { - const scan = (window as any).__REACT_SCAN__; - return ( - scan?.ReactScanInternals?.Store?.inspectState?.value?.kind === kind - ); + const scan = window.__REACT_SCAN__; + return scan?.ReactScanInternals?.Store?.inspectState?.value?.kind === kind; }, expectedKind, { timeout }, ); } -export async function isOutlineInstrumentationPaused( - page: Page, -): Promise { +export async function isOutlineInstrumentationPaused(page: Page): Promise { return page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; const instrumentation = scan?.ReactScanInternals?.instrumentation; return instrumentation?.isPaused?.value ?? null; }); @@ -138,6 +126,10 @@ export function toolbarWidget(page: Page): Locator { return page.locator(TOOLBAR_SELECTORS.widget); } +export function overlayPanel(page: Page): Locator { + return page.locator(TOOLBAR_SELECTORS.panel); +} + export function inspectButton(page: Page): Locator { return page.locator(TOOLBAR_SELECTORS.inspectButton); } @@ -151,7 +143,7 @@ export function outlineToggle(page: Page): Locator { } export async function waitForToolbarReady(page: Page): Promise { - await inspectButton(page).waitFor({ state: 'visible', timeout: 10_000 }); + await inspectButton(page).waitFor({ state: "visible", timeout: 10_000 }); } // When the widget is expanded, the absolutely-positioned resize handles sit on @@ -159,7 +151,7 @@ export async function waitForToolbarReady(page: Page): Promise { // Dispatching the click directly on the control drives its handler regardless // of layering, which is what we want when asserting control behavior. export async function clickToolbarControl(locator: Locator): Promise { - await locator.dispatchEvent('click'); + await locator.dispatchEvent("click"); } // The overlay buttons live in the shadow root, so clicking them makes the @@ -175,13 +167,10 @@ export async function blurOverlay(page: Page): Promise { export async function enterInspectMode(page: Page): Promise { await waitForToolbarReady(page); await inspectButton(page).click(); - await waitForInspectStateKind(page, 'inspecting'); + await waitForInspectStateKind(page, "inspecting"); } -export async function focusComponent( - page: Page, - targetSelector: string, -): Promise { +export async function focusComponent(page: Page, targetSelector: string): Promise { // While inspecting, the overlay event-catcher covers the page, so the // standard element-targeted click fails Playwright actionability checks. // Drive raw pointer coordinates instead: the overlay resolves the element @@ -199,5 +188,5 @@ export async function focusComponent( await page.mouse.move(centerX + 1, centerY + 1); await page.waitForTimeout(40); await page.mouse.click(centerX, centerY); - await waitForInspectStateKind(page, 'focused'); + await waitForInspectStateKind(page, "focused"); } diff --git a/e2e/inspector.spec.ts b/e2e/inspector.spec.ts index 15c25d70..e7837156 100644 --- a/e2e/inspector.spec.ts +++ b/e2e/inspector.spec.ts @@ -1,14 +1,14 @@ -import { test, expect } from '@playwright/test'; -import { gotoFixture } from './helpers'; +import { test, expect } from "./fixtures"; +import { gotoFixture } from "./helpers"; -test.describe('Inspector', () => { +test.describe("Inspector", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); - test('inspect state is available in React Scan internals', async ({ page }) => { + test("inspect state is available in React Scan internals", async ({ page }) => { const hasInspectState = await page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; if (!scan?.ReactScanInternals?.Store) return false; const inspectState = scan.ReactScanInternals.Store.inspectState; return inspectState !== undefined && inspectState !== null; @@ -17,29 +17,32 @@ test.describe('Inspector', () => { expect(hasInspectState).toBe(true); }); - test('inspect state starts as inspect-off', async ({ page }) => { + test("inspect state starts as inspect-off", async ({ page }) => { const kind = await page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; return scan?.ReactScanInternals?.Store?.inspectState?.value?.kind ?? null; }); - expect(kind).toBe('inspect-off'); + expect(kind).toBe("inspect-off"); }); - test('shadow DOM contains toolbar elements', async ({ page }) => { + test("shadow DOM contains toolbar elements", async ({ page }) => { const elementCount = await page.evaluate(() => { - const root = document.getElementById('react-scan-root'); - return root?.shadowRoot?.querySelectorAll('*').length ?? 0; + const root = document.getElementById("react-scan-root"); + return root?.shadowRoot?.querySelectorAll("*").length ?? 0; }); expect(elementCount).toBeGreaterThan(5); }); - test('inspect state can be set programmatically', async ({ page }) => { + test("inspect state can be set programmatically", async ({ page }) => { const activated = await page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; if (!scan?.ReactScanInternals?.Store?.inspectState) return false; - scan.ReactScanInternals.Store.inspectState.value = { kind: 'focused', focusedDomElement: null }; - return scan.ReactScanInternals.Store.inspectState.value.kind === 'focused'; + scan.ReactScanInternals.Store.inspectState.value = { + kind: "focused", + focusedDomElement: null, + }; + return scan.ReactScanInternals.Store.inspectState.value.kind === "focused"; }); expect(activated).toBe(true); diff --git a/e2e/notifications.spec.ts b/e2e/notifications.spec.ts index d0d28067..581fe3b4 100644 --- a/e2e/notifications.spec.ts +++ b/e2e/notifications.spec.ts @@ -1,33 +1,33 @@ -import { test, expect } from '@playwright/test'; -import { gotoFixture } from './helpers'; +import { test, expect } from "./fixtures"; +import { gotoFixture } from "./helpers"; -test.describe('Notifications', () => { +test.describe("Notifications", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); - test('slow interaction is detected and recorded', async ({ page }) => { + test("slow interaction is detected and recorded", async ({ page }) => { await page.click('[data-testid="trigger-slow"]'); await page.waitForTimeout(2000); const hasActiveStore = await page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; if (!scan?.ReactScanInternals?.Store) return false; // Verify the notification system is wired up (interactionListeningForRenders is a function when active) - return typeof scan.ReactScanInternals.Store.interactionListeningForRenders === 'function'; + return typeof scan.ReactScanInternals.Store.interactionListeningForRenders === "function"; }); expect(hasActiveStore).toBe(true); }); - test('notification system initializes with the toolbar', async ({ page }) => { + test("notification system initializes with the toolbar", async ({ page }) => { const hasCanvas = await page.evaluate(() => { - return document.querySelectorAll('canvas').length > 0; + return document.querySelectorAll("canvas").length > 0; }); expect(hasCanvas).toBe(true); }); - test('repeated slow interactions do not break the toolbar', async ({ page }) => { + test("repeated slow interactions do not break the toolbar", async ({ page }) => { for (let i = 0; i < 3; i++) { await page.click('[data-testid="trigger-slow"]'); await page.waitForTimeout(500); @@ -35,8 +35,8 @@ test.describe('Notifications', () => { await page.waitForTimeout(2000); const shadowContent = await page.evaluate(() => { - const root = document.getElementById('react-scan-root'); - return root?.shadowRoot?.innerHTML ?? ''; + const root = document.getElementById("react-scan-root"); + return root?.shadowRoot?.innerHTML ?? ""; }); expect(shadowContent.length).toBeGreaterThan(100); }); diff --git a/e2e/outlines.spec.ts b/e2e/outlines.spec.ts index 26135ea3..256c34ba 100644 --- a/e2e/outlines.spec.ts +++ b/e2e/outlines.spec.ts @@ -1,53 +1,52 @@ -import { test, expect, type Page } from '@playwright/test'; -import { gotoFixture, getRenderCount } from './helpers'; +import { expect, test } from "./fixtures"; +import type { Page } from "@playwright/test"; +import { gotoFixture, getRenderCount } from "./helpers"; -async function clickAndCountRenders( - page: Page, - selector: string, - waitMs = 1000, -): Promise { +async function clickAndCountRenders(page: Page, selector: string, waitMs = 1000): Promise { await page.evaluate(() => { - (window as any).__E2E_RENDER_COUNT__ = 0; + window.__E2E_RENDER_COUNT__ = 0; }); await page.click(selector); await page.waitForTimeout(waitMs); return getRenderCount(page); } -test.describe('Render Outlines', () => { +test.describe("Render Outlines", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); - test('state update triggers render tracking', async ({ page }) => { + test("state update triggers render tracking", async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="increment"]'); expect(count).toBeGreaterThan(0); }); - test('rapid updates produce multiple tracked renders', async ({ page }) => { + test("rapid updates produce multiple tracked renders", async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="trigger-rapid"]', 2000); expect(count).toBeGreaterThan(5); }); - test('outline canvas exists on the page', async ({ page }) => { + test("outline canvas exists on the page", async ({ page }) => { const hasCanvas = await page.evaluate(() => { - return document.querySelectorAll('canvas').length > 0; + return document.querySelectorAll("canvas").length > 0; }); expect(hasCanvas).toBe(true); }); - test('context change triggers render tracking', async ({ page }) => { + test("context change triggers render tracking", async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="toggle-theme"]'); expect(count).toBeGreaterThan(0); }); - test('unstable props on memo components trigger render tracking', async ({ page }) => { + test("unstable props on memo components trigger render tracking", async ({ page }) => { const count = await clickAndCountRenders(page, '[data-testid="trigger-unstable"]'); expect(count).toBeGreaterThan(0); }); - test('render count accumulates with repeated clicks', async ({ page }) => { - await page.evaluate(() => { (window as any).__E2E_RENDER_COUNT__ = 0; }); + test("render count accumulates with repeated clicks", async ({ page }) => { + await page.evaluate(() => { + window.__E2E_RENDER_COUNT__ = 0; + }); await page.click('[data-testid="increment"]'); await page.waitForTimeout(300); diff --git a/e2e/overlay-comprehensive.spec.ts b/e2e/overlay-comprehensive.spec.ts new file mode 100644 index 00000000..3cfd5903 --- /dev/null +++ b/e2e/overlay-comprehensive.spec.ts @@ -0,0 +1,407 @@ +import type { Page } from "@playwright/test"; +import { expect, test } from "./fixtures"; +import { + FIXTURE_URL, + TOOLBAR_SELECTORS, + enterInspectMode, + focusComponent, + getInspectStateKind, + getRenderCount, + gotoFixture, + inspectButton, + notificationsButton, + overlayPanel, + toolbarWidget, + waitForInspectStateKind, + waitForToolbarReady, +} from "./helpers"; + +const TOOLBAR_STATE_KEY = "react-scan-toolbar-state-v1"; +const WIDGET_SETTINGS_KEY = "react-scan-widget-settings-v2"; +const AUDIO_NOTIFICATIONS_KEY = "react-scan-notifications-audio"; +const SLOWDOWN_CAPTURE_WAIT_MS = 1_500; +const SNAP_ANIMATION_WAIT_MS = 500; +const PANEL_RESIZE_DISTANCE_PX = 80; + +const openFocusedInspector = async (page: Page): Promise => { + await enterInspectMode(page); + await focusComponent(page, '[data-testid="increment"]'); +}; + +const recordSlowdownAndOpenNotifications = async (page: Page): Promise => { + await page.getByTestId("trigger-slow").click(); + await page.waitForTimeout(SLOWDOWN_CAPTURE_WAIT_MS); + await notificationsButton(page).click(); + await expect(page.locator(`${TOOLBAR_SELECTORS.panel} >> text=History`).first()).toBeVisible(); + await expect + .poll(async () => page.locator(`${TOOLBAR_SELECTORS.panel} >> text=No Events`).count()) + .toBe(0); +}; + +const selectFirstSlowdown = async (page: Page): Promise => { + const slowdownButton = overlayPanel(page).locator("button").filter({ hasText: /\d+ms/ }).first(); + await expect(slowdownButton).toBeVisible(); + await slowdownButton.click(); + await expect(overlayPanel(page).getByText("Ranked", { exact: true })).toBeVisible(); +}; + +test.describe("Comprehensive toolbar interactions", () => { + test.beforeEach(async ({ page }) => { + await gotoFixture(page); + await waitForToolbarReady(page); + }); + + test("shows delayed tooltips for toolbar controls", async ({ page }) => { + await inspectButton(page).hover(); + await expect( + toolbarWidget(page).locator("div.pointer-events-none").filter({ hasText: "Inspect element" }), + ).toBeVisible(); + + await notificationsButton(page).hover(); + await expect( + toolbarWidget(page).locator("div.pointer-events-none").filter({ hasText: "Notifications" }), + ).toBeVisible(); + }); + + test("keeps expanded and collapsed toolbar surfaces rounded", async ({ page }) => { + const expandedSurface = toolbarWidget(page).locator(":scope > div").first(); + await expect + .poll(() => expandedSurface.evaluate((element) => getComputedStyle(element).borderRadius)) + .not.toBe("0px"); + await expect + .poll(() => + toolbarWidget(page).evaluate((element) => getComputedStyle(element).backgroundColor), + ) + .toBe("rgba(0, 0, 0, 0)"); + + await page.getByTitle("Collapse React Scan").click(); + const collapsedSurface = page.getByTitle("Expand React Scan"); + await expect + .poll(() => collapsedSurface.evaluate((element) => getComputedStyle(element).borderRadius)) + .not.toBe("0px"); + }); + + test("collapses, expands, and restores collapse state after reload", async ({ page }) => { + await page.getByTitle("Collapse React Scan").click(); + await expect(page.getByTitle("Expand React Scan")).toBeVisible(); + + await page.reload(); + await expect(page.getByTitle("Expand React Scan")).toBeVisible(); + await page.getByTitle("Expand React Scan").click(); + await expect(page.getByTitle("Collapse React Scan")).toBeVisible(); + + const collapsed = await page.evaluate((storageKey) => { + const storedValue = localStorage.getItem(storageKey); + return storedValue ? JSON.parse(storedValue).collapsed : null; + }, TOOLBAR_STATE_KEY); + expect(collapsed).toBe(false); + }); + + test("drags, snaps, persists, and restores the toolbar edge", async ({ page }) => { + const toolbarBounds = await toolbarWidget(page).boundingBox(); + const viewport = page.viewportSize(); + expect(toolbarBounds).not.toBeNull(); + expect(viewport).not.toBeNull(); + if (!toolbarBounds || !viewport) return; + + await page.mouse.move(toolbarBounds.x + 10, toolbarBounds.y + toolbarBounds.height / 2); + await page.mouse.down(); + await page.mouse.move(4, viewport.height / 2, { steps: 8 }); + await page.mouse.up(); + await page.waitForTimeout(SNAP_ANIMATION_WAIT_MS); + + await expect + .poll(() => + page.evaluate((storageKey) => { + const storedValue = localStorage.getItem(storageKey); + return storedValue ? JSON.parse(storedValue).edge : null; + }, TOOLBAR_STATE_KEY), + ) + .toBe("left"); + + await page.reload(); + await waitForToolbarReady(page); + await expect + .poll(async () => (await toolbarWidget(page).boundingBox())?.x ?? Infinity) + .toBeLessThan(30); + }); + + test("maximizes and restores panel width from the resize handle", async ({ page }) => { + await notificationsButton(page).click(); + const initialBounds = await overlayPanel(page).boundingBox(); + expect(initialBounds).not.toBeNull(); + if (!initialBounds) return; + + const leftResizeHandle = overlayPanel(page).locator(".resize-left"); + await leftResizeHandle.dblclick({ force: true }); + await expect + .poll(async () => (await overlayPanel(page).boundingBox())?.width ?? 0) + .toBeGreaterThan(initialBounds.width); + + const isFullWidth = await page.evaluate((storageKey) => { + const storedValue = localStorage.getItem(storageKey); + return storedValue ? Boolean(JSON.parse(storedValue).dimensions?.isFullWidth) : false; + }, WIDGET_SETTINGS_KEY); + expect(isFullWidth).toBe(true); + + await page.waitForTimeout(SNAP_ANIMATION_WAIT_MS); + const maximizedBounds = await overlayPanel(page).boundingBox(); + await leftResizeHandle.dispatchEvent("dblclick"); + await expect + .poll(async () => (await overlayPanel(page).boundingBox())?.width ?? Infinity) + .toBeLessThan(maximizedBounds?.width ?? Infinity); + }); + + test("resizes with a pointer drag while preserving rounded corners", async ({ page }) => { + await notificationsButton(page).click(); + const panel = overlayPanel(page); + const panelContent = panel.locator(".react-scan-panel-content"); + const topResizeHandle = panel.locator(".resize-top"); + const initialPanelBounds = await panel.boundingBox(); + const handleBounds = await topResizeHandle.boundingBox(); + expect(initialPanelBounds).not.toBeNull(); + expect(handleBounds).not.toBeNull(); + if (!initialPanelBounds || !handleBounds) return; + + expect(handleBounds.y).toBeLessThan(initialPanelBounds.y); + const initialBorderRadius = await panelContent.evaluate( + (element) => getComputedStyle(element).borderRadius, + ); + expect(initialBorderRadius).not.toBe("0px"); + + const handleCenterX = handleBounds.x + handleBounds.width / 2; + const handleCenterY = handleBounds.y + handleBounds.height / 2; + await page.mouse.move(handleCenterX, handleCenterY); + await page.mouse.down(); + await page.mouse.move(handleCenterX, handleCenterY - PANEL_RESIZE_DISTANCE_PX); + await page.mouse.up(); + + await expect + .poll(async () => (await panel.boundingBox())?.height ?? 0) + .toBeGreaterThan(initialPanelBounds.height); + + const leftResizeHandle = panel.locator(".resize-left"); + const leftHandleBounds = await leftResizeHandle.boundingBox(); + expect(leftHandleBounds).not.toBeNull(); + if (!leftHandleBounds) return; + const leftHandleCenterX = leftHandleBounds.x + leftHandleBounds.width / 2; + const leftHandleCenterY = leftHandleBounds.y + leftHandleBounds.height / 2; + await page.mouse.move(leftHandleCenterX, leftHandleCenterY); + await page.mouse.down(); + await page.mouse.move(leftHandleCenterX - PANEL_RESIZE_DISTANCE_PX, leftHandleCenterY); + await page.mouse.up(); + + await expect + .poll(async () => (await panel.boundingBox())?.width ?? 0) + .toBeGreaterThan(initialPanelBounds.width); + await expect + .poll(() => panelContent.evaluate((element) => getComputedStyle(element).borderRadius)) + .toBe(initialBorderRadius); + }); + + test("tracks host light and dark theme markers", async ({ page }) => { + const root = page.locator(TOOLBAR_SELECTORS.root); + await page.evaluate(() => { + document.documentElement.classList.add("dark"); + }); + await expect(root).toHaveAttribute("data-react-scan-theme", "light"); + + await page.evaluate(() => { + document.documentElement.classList.remove("dark"); + document.documentElement.classList.add("light"); + }); + await expect(root).toHaveAttribute("data-react-scan-theme", "dark"); + }); + + test("disposes and recreates the toolbar through public options", async ({ page }) => { + await page.getByTestId("hide-toolbar").click(); + await expect(page.locator(TOOLBAR_SELECTORS.root)).toHaveCount(0); + + await page.getByTestId("show-toolbar").click(); + await waitForToolbarReady(page); + await expect(page.locator(TOOLBAR_SELECTORS.root)).toHaveCount(1); + }); +}); + +test.describe("Comprehensive inspector interactions", () => { + test.beforeEach(async ({ page }) => { + await gotoFixture(page); + await waitForToolbarReady(page); + }); + + test("copies a focused element and closes after feedback", async ({ context, page }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await openFocusedInspector(page); + + await page.locator('.react-scan-close-button[title^="Copy element"]').dispatchEvent("click"); + await expect + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toContain("Counter"); + await waitForInspectStateKind(page, "inspect-off"); + }); + + test("copies with the keyboard shortcut", async ({ context, page }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await openFocusedInspector(page); + + await page.keyboard.press("ControlOrMeta+C"); + await expect + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toContain("Counter"); + }); + + test("searches component names and navigates matches", async ({ page }) => { + await openFocusedInspector(page); + const searchInput = page.locator('input[placeholder="Component name, /regex/, or [type]"]'); + + await searchInput.fill("Counter"); + await expect(page.locator(".react-scan-components-tree").getByText(/\d+\|\d+/)).toBeVisible(); + await searchInput.press("Enter"); + await searchInput.press("Shift+Enter"); + await searchInput.press("Control+Enter"); + expect(await getInspectStateKind(page)).toBe("focused"); + + await inspectButton(page).dispatchEvent("click"); + await waitForInspectStateKind(page, "inspecting"); + await focusComponent(page, '[data-testid="memo-child"]'); + await searchInput.fill("[memo]"); + await expect(page.locator(".react-scan-components-tree").getByText(/\d+\|\d+/)).toBeVisible(); + await searchInput.press("Control+Enter"); + await expect(page.locator(".react-scan-header span[title*='MemoChild']")).toBeVisible(); + }); + + test("keeps copy shortcuts scoped away from search input", async ({ context, page }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await openFocusedInspector(page); + const searchInput = page.locator('input[placeholder="Component name, /regex/, or [type]"]'); + await searchInput.fill("Counter"); + await searchInput.focus(); + + await page.keyboard.press("ControlOrMeta+C"); + expect(await getInspectStateKind(page)).toBe("focused"); + await expect(searchInput).toBeFocused(); + }); + + test("shows prop changes after a focused memo component rerenders", async ({ page }) => { + await enterInspectMode(page); + await focusComponent(page, '[data-testid="memo-child"]'); + await page.getByTestId("trigger-unstable").dispatchEvent("click"); + + await expect( + page.locator(TOOLBAR_SELECTORS.inspectorPanel).getByText("Changed Props", { exact: true }), + ).toBeVisible(); + }); +}); + +test.describe("Comprehensive notification interactions", () => { + test.beforeEach(async ({ page }) => { + await gotoFixture(page); + await waitForToolbarReady(page); + }); + + test("toggles and restores audio alerts", async ({ page }) => { + await notificationsButton(page).click(); + const enableAudioButton = overlayPanel(page).getByRole("button", { + name: "Enable audio alerts", + }); + await enableAudioButton.click(); + await expect( + overlayPanel(page).getByRole("button", { + name: "Disable audio alerts", + }), + ).toBeVisible(); + expect( + await page.evaluate( + (storageKey) => localStorage.getItem(storageKey), + AUDIO_NOTIFICATIONS_KEY, + ), + ).toBe("true"); + + await page.reload(); + await waitForToolbarReady(page); + await notificationsButton(page).click(); + const disableAudioButton = overlayPanel(page).getByRole("button", { + name: "Disable audio alerts", + }); + await disableAudioButton.click(); + expect( + await page.evaluate( + (storageKey) => localStorage.getItem(storageKey), + AUDIO_NOTIFICATIONS_KEY, + ), + ).toBe("false"); + }); + + test("selects a slowdown and switches visualization routes", async ({ page }) => { + await recordSlowdownAndOpenNotifications(page); + await selectFirstSlowdown(page); + + await overlayPanel(page).getByText("Overview", { exact: true }).click(); + await expect(page.locator("#overview-scroll-container")).toBeVisible(); + await expect(page.locator('[id^="react-scan-overview-bar-"]').first()).toBeVisible(); + + await overlayPanel(page).getByText("Ranked", { exact: true }).click(); + await expect(overlayPanel(page).getByText("Ranked", { exact: true })).toBeVisible(); + }); + + test("opens render explanations and returns to ranked renders", async ({ page }) => { + await recordSlowdownAndOpenNotifications(page); + await selectFirstSlowdown(page); + + const renderBar = overlayPanel(page).locator('button[class*="w-[90%]"]').first(); + await expect(renderBar).toBeVisible(); + await renderBar.click(); + await expect( + overlayPanel(page).getByText(/No changes detected|How to stop renders/), + ).toBeVisible(); + + await overlayPanel(page).getByRole("button", { name: "Overview" }).last().click(); + await expect(overlayPanel(page).getByText("Ranked", { exact: true })).toBeVisible(); + }); + + test("groups and expands repeated keyboard slowdowns", async ({ page }) => { + await page.getByTestId("slow-input").pressSequentially("abc"); + await page.waitForTimeout(SLOWDOWN_CAPTURE_WAIT_MS); + await notificationsButton(page).click(); + + const collapsedKeyboardEvent = overlayPanel(page) + .locator("button") + .filter({ hasText: "SlowInput" }) + .first(); + await expect(collapsedKeyboardEvent).toContainText("x3"); + await collapsedKeyboardEvent.click(); + await expect(overlayPanel(page).locator("button").filter({ hasText: /\d+ms/ })).toHaveCount(3); + }); + + test("clears slowdown history and resets selected details", async ({ page }) => { + await recordSlowdownAndOpenNotifications(page); + await selectFirstSlowdown(page); + + await overlayPanel(page).getByTitle("Clear all events").click(); + await expect(overlayPanel(page).getByText("No Events")).toBeVisible(); + await expect(overlayPanel(page).getByText(/Scanning for slowdowns/)).toBeVisible(); + }); + + test("closes notifications from a selected event header", async ({ page }) => { + await recordSlowdownAndOpenNotifications(page); + await selectFirstSlowdown(page); + + await overlayPanel(page).getByTitle("Close").click(); + await expect(overlayPanel(page)).toBeHidden(); + await expect(notificationsButton(page)).toHaveAttribute("aria-pressed", "false"); + }); +}); + +test("renders outlines on the main thread when workers are disabled", async ({ page }) => { + await gotoFixture(page, `${FIXTURE_URL}&outlines=main-thread`); + await waitForToolbarReady(page); + + expect( + await page.evaluate( + () => window.__REACT_SCAN__?.ReactScanInternals?.options?.value?.useOffscreenCanvasWorker, + ), + ).toBe(false); + await page.getByTestId("increment").click(); + await expect.poll(() => getRenderCount(page)).toBeGreaterThan(0); + await expect(page.locator("canvas").first()).toBeAttached(); +}); diff --git a/e2e/overlay-inspector.spec.ts b/e2e/overlay-inspector.spec.ts index 2db4f65d..8cd9667d 100644 --- a/e2e/overlay-inspector.spec.ts +++ b/e2e/overlay-inspector.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "./fixtures"; import { gotoFixture, enterInspectMode, @@ -10,29 +10,29 @@ import { waitForInspectStateKind, waitForToolbarReady, TOOLBAR_SELECTORS, -} from './helpers'; +} from "./helpers"; const COMPONENT_TREE_SEARCH = 'input[placeholder="Component name, /regex/, or [type]"]'; -test.describe('Overlay inspector flow', () => { +test.describe("Overlay inspector flow", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); await waitForToolbarReady(page); }); - test('inspecting mode activates the overlay canvas', async ({ page }) => { + test("inspecting mode activates the overlay canvas", async ({ page }) => { await enterInspectMode(page); const overlay = page.locator(TOOLBAR_SELECTORS.overlayCanvas); await expect(overlay).toHaveCount(1); }); - test('clicking a component while inspecting focuses it', async ({ page }) => { + test("clicking a component while inspecting focuses it", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); - expect(await getInspectStateKind(page)).toBe('focused'); + expect(await getInspectStateKind(page)).toBe("focused"); }); - test('focusing a component opens the inspector panel', async ({ page }) => { + test("focusing a component opens the inspector panel", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); @@ -40,68 +40,60 @@ test.describe('Overlay inspector flow', () => { await expect(page.locator(COMPONENT_TREE_SEARCH)).toBeVisible(); }); - test('inspector header shows the focused component name', async ({ page }) => { + test("inspector header shows the focused component name", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); - await expect( - page.locator(`${TOOLBAR_SELECTORS.widget} >> text=Counter`).first(), - ).toBeVisible(); + await expect(page.locator(`${TOOLBAR_SELECTORS.panel} >> text=Counter`).first()).toBeVisible(); }); - test('a copy button appears when a component is focused', async ({ page }) => { + test("a copy button appears when a component is focused", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); - await expect( - page.locator('.react-scan-close-button[title*="Copy element"]'), - ).toBeVisible(); + await expect(page.locator('.react-scan-close-button[title*="Copy element"]')).toBeVisible(); }); - test('close button exits inspection back to inspect-off', async ({ page }) => { + test("close button exits inspection back to inspect-off", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); await clickToolbarControl(page.locator(TOOLBAR_SELECTORS.closeButton)); - await waitForInspectStateKind(page, 'inspect-off'); - expect(await getInspectStateKind(page)).toBe('inspect-off'); + await waitForInspectStateKind(page, "inspect-off"); + expect(await getInspectStateKind(page)).toBe("inspect-off"); }); - test('pressing Escape returns from focused to inspecting', async ({ page }) => { + test("pressing Escape returns from focused to inspecting", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); await blurOverlay(page); - await page.keyboard.press('Escape'); - await waitForInspectStateKind(page, 'inspecting'); - expect(await getInspectStateKind(page)).toBe('inspecting'); + await page.keyboard.press("Escape"); + await waitForInspectStateKind(page, "inspecting"); + expect(await getInspectStateKind(page)).toBe("inspecting"); }); - test('re-focusing a different component keeps the inspector open', async ({ - page, - }) => { + test("re-focusing a different component keeps the inspector open", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); - expect(await getInspectStateKind(page)).toBe('focused'); + expect(await getInspectStateKind(page)).toBe("focused"); await blurOverlay(page); - await page.keyboard.press('Escape'); - await waitForInspectStateKind(page, 'inspecting'); + await page.keyboard.press("Escape"); + await waitForInspectStateKind(page, "inspecting"); await focusComponent(page, '[data-testid="toggle-theme"]'); - expect(await getInspectStateKind(page)).toBe('focused'); + expect(await getInspectStateKind(page)).toBe("focused"); await expect(page.locator(TOOLBAR_SELECTORS.inspectorPanel)).toBeVisible(); }); - test('toggling inspect button off while focused tears down the panel', async ({ - page, - }) => { + test("toggling inspect button off while focused tears down the panel", async ({ page }) => { await enterInspectMode(page); await focusComponent(page, '[data-testid="increment"]'); // From "focused" the inspect button advances to "inspecting". await clickToolbarControl(inspectButton(page)); - await waitForInspectStateKind(page, 'inspecting'); - expect(await getInspectStateKind(page)).toBe('inspecting'); + await waitForInspectStateKind(page, "inspecting"); + expect(await getInspectStateKind(page)).toBe("inspecting"); }); }); diff --git a/e2e/overlay-notifications.spec.ts b/e2e/overlay-notifications.spec.ts index 9975d771..ba5a721b 100644 --- a/e2e/overlay-notifications.spec.ts +++ b/e2e/overlay-notifications.spec.ts @@ -1,70 +1,55 @@ -import { test, expect, type Page } from '@playwright/test'; +import { expect, test } from "./fixtures"; +import type { Page } from "@playwright/test"; import { gotoFixture, notificationsButton, - toolbarWidget, getInspectStateKind, waitForToolbarReady, TOOLBAR_SELECTORS, -} from './helpers'; +} from "./helpers"; const historyHeader = (page: Page) => - page.locator(`${TOOLBAR_SELECTORS.widget} >> text=History`).first(); + page.locator(`${TOOLBAR_SELECTORS.panel} >> text=History`).first(); -const widgetHeight = async (page: Page): Promise => { - const box = await toolbarWidget(page).boundingBox(); - return box?.height ?? 0; -}; - -test.describe('Overlay notifications panel', () => { +test.describe("Overlay notifications panel", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); await waitForToolbarReady(page); }); - test('clicking the notifications button opens the panel', async ({ page }) => { + test("clicking the notifications button opens the panel", async ({ page }) => { await notificationsButton(page).click(); await expect(historyHeader(page)).toBeVisible(); }); - test('panel renders the slowdown history chrome', async ({ page }) => { + test("panel renders the slowdown history chrome", async ({ page }) => { await notificationsButton(page).click(); await expect(historyHeader(page)).toBeVisible(); // The "Clear all events" control is always present in the history panel, // unlike the "No Events" empty state which disappears if React Scan // incidentally records a slow render (e.g. under CPU contention). await expect( - page.locator( - `${TOOLBAR_SELECTORS.widget} button[title="Clear all events"]`, - ), + page.locator(`${TOOLBAR_SELECTORS.panel} button[title="Clear all events"]`), ).toBeVisible(); }); - test('clicking the notifications button again closes the panel', async ({ - page, - }) => { - const minimizedHeight = await widgetHeight(page); - + test("clicking the notifications button again closes the panel", async ({ page }) => { + const panel = page.locator(TOOLBAR_SELECTORS.panel); + await expect(panel).toBeHidden(); await notificationsButton(page).click(); - await expect - .poll(async () => widgetHeight(page)) - .toBeGreaterThan(minimizedHeight + 50); + await expect(panel).toBeVisible(); await notificationsButton(page).click(); - await expect - .poll(async () => widgetHeight(page)) - .toBeLessThan(minimizedHeight + 50); + await expect(panel).toBeHidden(); }); - test('opening notifications turns inspection off', async ({ page }) => { + test("opening notifications turns inspection off", async ({ page }) => { await notificationsButton(page).click(); await expect(historyHeader(page)).toBeVisible(); - expect(await getInspectStateKind(page)).toBe('inspect-off'); + expect(await getInspectStateKind(page)).toBe("inspect-off"); }); - test('a slow interaction is recorded and surfaced in the panel', async ({ - page, - }) => { + test("a slow interaction is recorded and surfaced in the panel", async ({ page }) => { await page.click('[data-testid="trigger-slow"]'); await page.waitForTimeout(1500); @@ -72,13 +57,9 @@ test.describe('Overlay notifications panel', () => { await expect(historyHeader(page)).toBeVisible(); await expect - .poll( - async () => - page - .locator(`${TOOLBAR_SELECTORS.widget} >> text=No Events`) - .count(), - { timeout: 10_000 }, - ) + .poll(async () => page.locator(`${TOOLBAR_SELECTORS.panel} >> text=No Events`).count(), { + timeout: 10_000, + }) .toBe(0); }); }); diff --git a/e2e/overlay-toolbar.spec.ts b/e2e/overlay-toolbar.spec.ts index f2411a0e..c1100462 100644 --- a/e2e/overlay-toolbar.spec.ts +++ b/e2e/overlay-toolbar.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "./fixtures"; import { gotoFixture, toolbarWidget, @@ -9,111 +9,99 @@ import { waitForInspectStateKind, waitForToolbarReady, isOutlineInstrumentationPaused, - TOOLBAR_SELECTORS, -} from './helpers'; +} from "./helpers"; -test.describe('Overlay toolbar UI', () => { +test.describe("Overlay toolbar UI", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); await waitForToolbarReady(page); }); - test('renders the toolbar widget and its core controls', async ({ page }) => { + test("renders the toolbar widget and its core controls", async ({ page }) => { await expect(toolbarWidget(page)).toBeVisible(); await expect(inspectButton(page)).toBeVisible(); await expect(notificationsButton(page)).toBeVisible(); - await expect(inspectButton(page)).toHaveAttribute('title', 'Inspect element'); - await expect(notificationsButton(page)).toHaveAttribute( - 'title', - 'Notifications', - ); + await expect(inspectButton(page)).toHaveAttribute("title", "Inspect element"); + await expect(notificationsButton(page)).toHaveAttribute("title", "Notifications"); }); - test('FPS meter is rendered in the toolbar', async ({ page }) => { + test("FPS meter is rendered in the toolbar", async ({ page }) => { // The meter starts blank and only paints once its 200ms sampling interval // ticks, which can lag under CPU contention, so poll the widget text. await expect - .poll( - async () => - toolbarWidget(page).evaluate((el) => el.textContent ?? ''), - { timeout: 20_000 }, - ) - .toContain('FPS'); + .poll(async () => toolbarWidget(page).evaluate((el) => el.textContent ?? ""), { + timeout: 20_000, + }) + .toContain("FPS"); }); - test('clicking inspect button enters inspecting mode', async ({ page }) => { - expect(await getInspectStateKind(page)).toBe('inspect-off'); + test("clicking inspect button enters inspecting mode", async ({ page }) => { + expect(await getInspectStateKind(page)).toBe("inspect-off"); await inspectButton(page).click(); - await waitForInspectStateKind(page, 'inspecting'); - expect(await getInspectStateKind(page)).toBe('inspecting'); + await waitForInspectStateKind(page, "inspecting"); + expect(await getInspectStateKind(page)).toBe("inspecting"); }); - test('inspect button icon color reflects inspecting state', async ({ - page, - }) => { - const inactiveColor = await inspectButton(page).evaluate( - (el) => (el as HTMLElement).style.color, - ); - expect(inactiveColor).toBe('rgb(153, 153, 153)'); - + test("inspect button exposes its active state", async ({ page }) => { + await expect(inspectButton(page)).toHaveAttribute("aria-pressed", "false"); await inspectButton(page).click(); - await waitForInspectStateKind(page, 'inspecting'); - - await expect - .poll(async () => - inspectButton(page).evaluate((el) => (el as HTMLElement).style.color), - ) - .toBe('rgb(142, 97, 227)'); + await waitForInspectStateKind(page, "inspecting"); + await expect(inspectButton(page)).toHaveAttribute("aria-pressed", "true"); }); - test('clicking inspect button twice toggles inspecting off', async ({ - page, - }) => { + test("clicking inspect button twice toggles inspecting off", async ({ page }) => { await inspectButton(page).click(); - await waitForInspectStateKind(page, 'inspecting'); + await waitForInspectStateKind(page, "inspecting"); await inspectButton(page).click(); - await waitForInspectStateKind(page, 'inspect-off'); - expect(await getInspectStateKind(page)).toBe('inspect-off'); + await waitForInspectStateKind(page, "inspect-off"); + expect(await getInspectStateKind(page)).toBe("inspect-off"); }); - test('outline toggle starts enabled (instrumentation active)', async ({ - page, - }) => { + test("outline toggle starts enabled (instrumentation active)", async ({ page }) => { await expect(outlineToggle(page)).toBeChecked(); expect(await isOutlineInstrumentationPaused(page)).toBe(false); }); - test('toggling outlines off pauses instrumentation and persists', async ({ - page, - }) => { + test("keeps the outline toggle thumb centered in both states", async ({ page }) => { + const getThumbTranslation = () => + outlineToggle(page).evaluate((input) => { + const track = input.nextElementSibling; + if (!(track instanceof HTMLElement)) return null; + const transform = getComputedStyle(track, "::before").transform; + const matrix = new DOMMatrixReadOnly(transform); + return { x: matrix.m41, y: matrix.m42 }; + }); + + const checkedTranslation = await getThumbTranslation(); + expect(checkedTranslation?.x).toBeGreaterThan(0); + expect(checkedTranslation?.y).toBeLessThan(0); + await outlineToggle(page).click(); + await expect.poll(async () => (await getThumbTranslation())?.x).toBe(0); + expect((await getThumbTranslation())?.y).toBeLessThan(0); + }); - await expect - .poll(async () => isOutlineInstrumentationPaused(page)) - .toBe(true); + test("toggling outlines off pauses instrumentation and persists", async ({ page }) => { + await outlineToggle(page).click(); + + await expect.poll(async () => isOutlineInstrumentationPaused(page)).toBe(true); await expect(outlineToggle(page)).not.toBeChecked(); const persistedEnabled = await page.evaluate(() => { - const raw = localStorage.getItem('react-scan-options'); + const raw = localStorage.getItem("react-scan-options"); return raw ? JSON.parse(raw).enabled : null; }); expect(persistedEnabled).toBe(false); }); - test('toggling outlines off then on restores instrumentation', async ({ - page, - }) => { + test("toggling outlines off then on restores instrumentation", async ({ page }) => { await outlineToggle(page).click(); - await expect - .poll(async () => isOutlineInstrumentationPaused(page)) - .toBe(true); + await expect.poll(async () => isOutlineInstrumentationPaused(page)).toBe(true); await outlineToggle(page).click(); - await expect - .poll(async () => isOutlineInstrumentationPaused(page)) - .toBe(false); + await expect.poll(async () => isOutlineInstrumentationPaused(page)).toBe(false); await expect(outlineToggle(page)).toBeChecked(); }); }); diff --git a/e2e/overlay-widget.spec.ts b/e2e/overlay-widget.spec.ts index 570480bb..45d582ed 100644 --- a/e2e/overlay-widget.spec.ts +++ b/e2e/overlay-widget.spec.ts @@ -1,53 +1,46 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "./fixtures"; import { gotoFixture, + overlayPanel, toolbarWidget, notificationsButton, isReactScanActive, waitForToolbarReady, TOOLBAR_SELECTORS, -} from './helpers'; +} from "./helpers"; -const LOCALSTORAGE_WIDGET_KEY = 'react-scan-widget-settings-v2'; +const LOCALSTORAGE_WIDGET_KEY = "react-scan-widget-settings-v2"; +const LOCALSTORAGE_TOOLBAR_KEY = "react-scan-toolbar-state-v1"; -test.describe('Overlay widget container', () => { +test.describe("Overlay widget container", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); await waitForToolbarReady(page); }); - test('widget mounts with the expected identity attributes', async ({ - page, - }) => { + test("widget mounts with the expected identity attributes", async ({ page }) => { const widget = toolbarWidget(page); await expect(widget).toBeVisible(); - await expect(widget).toHaveAttribute('dir', 'ltr'); + await expect(widget).toHaveAttribute("dir", "ltr"); }); - test('widget fades in to full opacity', async ({ page }) => { + test("widget fades in to full opacity", async ({ page }) => { await expect - .poll(async () => - toolbarWidget(page).evaluate((el) => - Number(getComputedStyle(el).opacity), - ), - ) + .poll(async () => toolbarWidget(page).evaluate((el) => Number(getComputedStyle(el).opacity))) .toBeGreaterThan(0.9); }); - test('all four resize handles are present in the DOM', async ({ page }) => { + test("all four resize handles are present in the DOM", async ({ page }) => { const handles = page.locator( - `${TOOLBAR_SELECTORS.root} .resize-left, ${TOOLBAR_SELECTORS.root} .resize-right, ${TOOLBAR_SELECTORS.root} .resize-top, ${TOOLBAR_SELECTORS.root} .resize-bottom`, + `${TOOLBAR_SELECTORS.panel} .resize-left, ${TOOLBAR_SELECTORS.panel} .resize-right, ${TOOLBAR_SELECTORS.panel} .resize-top, ${TOOLBAR_SELECTORS.panel} .resize-bottom`, ); await expect(handles).toHaveCount(4); }); - test('widget settings are persisted to localStorage', async ({ page }) => { + test("widget settings are persisted to localStorage", async ({ page }) => { await expect .poll(async () => - page.evaluate( - (key) => localStorage.getItem(key) !== null, - LOCALSTORAGE_WIDGET_KEY, - ), + page.evaluate((key) => localStorage.getItem(key) !== null, LOCALSTORAGE_WIDGET_KEY), ) .toBe(true); @@ -60,7 +53,7 @@ test.describe('Overlay widget container', () => { expect(settings.dimensions).toBeTruthy(); }); - test('widget survives repeated host-app interactions', async ({ page }) => { + test("widget survives repeated host-app interactions", async ({ page }) => { for (let clickIndex = 0; clickIndex < 5; clickIndex++) { await page.click('[data-testid="increment"]'); } @@ -68,18 +61,26 @@ test.describe('Overlay widget container', () => { expect(await isReactScanActive(page)).toBe(true); }); - test('opening a panel expands the widget', async ({ page }) => { - const minimizedBox = await toolbarWidget(page).boundingBox(); - expect(minimizedBox).not.toBeNull(); + test("opening a panel leaves the toolbar compact", async ({ page }) => { + const toolbarBox = await toolbarWidget(page).boundingBox(); + expect(toolbarBox).not.toBeNull(); + await expect(overlayPanel(page)).toBeHidden(); await notificationsButton(page).click(); - await page.waitForTimeout(600); + await expect(overlayPanel(page)).toBeVisible(); - await expect - .poll(async () => { - const box = await toolbarWidget(page).boundingBox(); - return box?.width ?? 0; - }) - .toBeGreaterThan(minimizedBox?.width ?? 0); + const openToolbarBox = await toolbarWidget(page).boundingBox(); + expect(openToolbarBox?.width).toBeCloseTo(toolbarBox?.width ?? 0, 0); + }); + + test("toolbar collapse state is persisted", async ({ page }) => { + await page.getByTitle("Collapse React Scan").click(); + await expect(page.getByTitle("Expand React Scan")).toBeVisible(); + + const state = await page.evaluate((key) => { + const rawState = localStorage.getItem(key); + return rawState ? JSON.parse(rawState) : null; + }, LOCALSTORAGE_TOOLBAR_KEY); + expect(state?.collapsed).toBe(true); }); }); diff --git a/e2e/toolbar.spec.ts b/e2e/toolbar.spec.ts index 34e147bd..66a1736c 100644 --- a/e2e/toolbar.spec.ts +++ b/e2e/toolbar.spec.ts @@ -1,19 +1,19 @@ -import { test, expect } from '@playwright/test'; -import { gotoFixture, isReactScanActive, hasShadowRoot } from './helpers'; +import { test, expect } from "./fixtures"; +import { gotoFixture, isReactScanActive, hasShadowRoot } from "./helpers"; -test.describe('Toolbar', () => { +test.describe("Toolbar", () => { test.beforeEach(async ({ page }) => { await gotoFixture(page); }); - test('React Scan initializes and attaches to the page', async ({ page }) => { + test("React Scan initializes and attaches to the page", async ({ page }) => { const active = await isReactScanActive(page); expect(active).toBe(true); }); - test('React Scan internals are accessible', async ({ page }) => { + test("React Scan internals are accessible", async ({ page }) => { const hasInternals = await page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; return ( scan?.ReactScanInternals !== undefined && scan.ReactScanInternals.options !== undefined && @@ -23,9 +23,9 @@ test.describe('Toolbar', () => { expect(hasInternals).toBe(true); }); - test('options are set correctly', async ({ page }) => { + test("options are set correctly", async ({ page }) => { const options = await page.evaluate(() => { - const scan = (window as any).__REACT_SCAN__; + const scan = window.__REACT_SCAN__; const opts = scan?.ReactScanInternals?.options?.value; if (!opts) return null; return { @@ -41,21 +41,21 @@ test.describe('Toolbar', () => { }); }); - test('shadow DOM root is created', async ({ page }) => { + test("shadow DOM root is created", async ({ page }) => { await page.waitForTimeout(1000); expect(await hasShadowRoot(page)).toBe(true); }); - test('toolbar has content in shadow DOM', async ({ page }) => { + test("toolbar has content in shadow DOM", async ({ page }) => { await page.waitForTimeout(1000); const childCount = await page.evaluate(() => { - const root = document.getElementById('react-scan-root'); + const root = document.getElementById("react-scan-root"); return root?.shadowRoot?.children.length ?? 0; }); expect(childCount).toBeGreaterThan(0); }); - test('toolbar persists across interactions', async ({ page }) => { + test("toolbar persists across interactions", async ({ page }) => { await page.click('[data-testid="increment"]'); await page.waitForTimeout(500); @@ -63,7 +63,7 @@ test.describe('Toolbar', () => { expect(active).toBe(true); const options = await page.evaluate(() => { - return (window as any).__REACT_SCAN__?.ReactScanInternals?.options?.value?.enabled; + return window.__REACT_SCAN__?.ReactScanInternals?.options?.value?.enabled; }); expect(options).toBe(true); }); diff --git a/kitchen-sink/src/examples/e2e-fixture/index.tsx b/kitchen-sink/src/examples/e2e-fixture/index.tsx index 8d976eae..6960cf0e 100644 --- a/kitchen-sink/src/examples/e2e-fixture/index.tsx +++ b/kitchen-sink/src/examples/e2e-fixture/index.tsx @@ -1,13 +1,15 @@ -import { useState, useContext, createContext, memo } from 'react'; -import { scan, Store } from 'react-scan'; +import { useState, useContext, createContext, memo, type JSX } from "react"; +import { scan, setOptions, Store } from "react-scan"; Store.isInIframe.value = false; scan({ enabled: true, dangerouslyForceRunInProduction: true, + useOffscreenCanvasWorker: + new URLSearchParams(window.location.search).get("outlines") !== "main-thread", }); -const ThemeContext = createContext('light'); +const ThemeContext = createContext("light"); function Counter(): JSX.Element { const [count, setCount] = useState(0); @@ -28,7 +30,7 @@ function UnstableProps(): JSX.Element { - {}} label="unstable" /> + {}} label="unstable" /> ); } @@ -55,14 +57,14 @@ function ContextConsumer(): JSX.Element { } function ThemeToggle(): JSX.Element { - const [theme, setTheme] = useState('light'); + const [theme, setTheme] = useState("light"); return (
@@ -75,13 +77,16 @@ function ThemeToggle(): JSX.Element { function SlowComponent(): JSX.Element { const [rendering, setRendering] = useState(false); - const triggerSlowRender = () => { - setRendering(true); + if (rendering) { const start = performance.now(); while (performance.now() - start < 100) { - // block for 100ms to simulate slow render + continue; } - setRendering(false); + } + + const triggerSlowRender = () => { + setRendering(true); + setTimeout(() => setRendering(false), 0); }; return ( @@ -89,7 +94,7 @@ function SlowComponent(): JSX.Element { - {rendering ? 'Rendering...' : 'Idle'} + {rendering ? "Rendering..." : "Idle"}
); } @@ -113,9 +118,27 @@ function RapidUpdater(): JSX.Element { ); } +const SlowInput = (): JSX.Element => { + const [value, setValue] = useState(""); + + return ( + { + const start = performance.now(); + while (performance.now() - start < 100) { + continue; + } + }} + onChange={(event) => setValue(event.currentTarget.value)} + /> + ); +}; + export default function E2EFixture(): JSX.Element { return ( -
+

React Scan E2E Fixture


@@ -142,6 +165,29 @@ export default function E2EFixture(): JSX.Element {

Rapid Updates

+
+
+

Runtime Controls

+ + +
+
+
+

Slow Keyboard Input

+ +
); } diff --git a/kitchen-sink/vite.config.ts b/kitchen-sink/vite.config.ts index 821ca578..0decea93 100644 --- a/kitchen-sink/vite.config.ts +++ b/kitchen-sink/vite.config.ts @@ -1,11 +1,15 @@ -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], + optimizeDeps: { + noDiscovery: true, + include: ["react", "react-dom", "react-dom/client", "react/jsx-dev-runtime"], + }, css: { modules: { - localsConvention: 'camelCaseOnly', + localsConvention: "camelCaseOnly", }, }, }); diff --git a/package.json b/package.json index d582f416..33b81ab4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "format:check": "vp fmt --check", "check": "vp check", "test:e2e": "playwright test", + "test:e2e:coverage": "COVERAGE=1 playwright test", "test:e2e:ui": "playwright test --ui", "changeset": "changeset add", "version": "changeset version", @@ -22,8 +23,9 @@ "@playwright/test": "^1.59.1", "@types/node": "^25.6.0", "@voidzero-dev/vite-plus-core": "^0.1.12", + "monocart-coverage-reports": "^2.12.12", "turbo": "^2.9.3", - "typescript": "latest", + "typescript": "6.0.3", "vite-plus": "^0.1.12" }, "engines": { diff --git a/packages/scan/package.json b/packages/scan/package.json index e899003b..e279065b 100644 --- a/packages/scan/package.json +++ b/packages/scan/package.json @@ -74,9 +74,9 @@ "default": "./dist/index.mjs" }, "require": { - "types": "./dist/index.d.mts", + "types": "./dist/index.d.ts", "react-server": "./dist/rsc-shim.js", - "default": "./dist/index.mjs" + "default": "./dist/index.js" } }, "development": { @@ -122,24 +122,24 @@ "./auto": { "production": { "import": { - "types": "./dist/rsc-shim.d.mts", + "types": "./dist/index.d.mts", "react-server": "./dist/rsc-shim.mjs", "default": "./dist/rsc-shim.mjs" }, "require": { - "types": "./dist/rsc-shim.d.ts", + "types": "./dist/index.d.ts", "react-server": "./dist/rsc-shim.js", "default": "./dist/rsc-shim.js" } }, "development": { "import": { - "types": "./dist/auto.d.mts", + "types": "./dist/index.d.mts", "react-server": "./dist/rsc-shim.mjs", "default": "./dist/auto.mjs" }, "require": { - "types": "./dist/auto.d.ts", + "types": "./dist/index.d.ts", "react-server": "./dist/rsc-shim.js", "default": "./dist/auto.js" } @@ -193,12 +193,13 @@ "access": "public" }, "scripts": { - "build": "pnpm build:css && NODE_ENV=production tsup", + "build": "pnpm build:css && NODE_ENV=production vp pack && node scripts/finalize-declarations.mjs", + "build:coverage": "pnpm build:css && NODE_ENV=production REACT_SCAN_COVERAGE=true vp pack", "build:copy": "pnpm build && cat dist/auto.global.js | pbcopy", "build:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css", "dev:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css --watch", - "dev:tsup": "NODE_ENV=development tsup --watch", - "dev": "pnpm run --parallel \"/^dev:(css|tsup)/\"", + "dev:pack": "NODE_ENV=development vp pack --watch", + "dev": "pnpm run --parallel \"/^dev:(css|pack)/\"", "pack": "npm version patch && pnpm build && npm pack", "pack:bump": "node scripts/bump-version.mjs && pnpm run pack && echo $(pwd)/react-scan-$(node -p \"require('./package.json').version\").tgz | pbcopy", "publint": "publint", @@ -211,28 +212,24 @@ "dependencies": { "@babel/core": "^7.29.0", "@babel/types": "^7.29.0", - "@preact/signals": "^2.9.0", "@rollup/pluginutils": "^5.3.0", - "bippy": "^0.5.39", + "bippy": "^0.6.0", "commander": "^14.0.0", "picocolors": "^1.1.1", - "preact": "^10.29.1", "prompts": "^2.4.2", - "react-doctor": "latest", - "react-grab": "latest" + "react-grab": "latest", + "solid-js": "^1.9.14" }, "devDependencies": { - "@esbuild-plugins/tsconfig-paths": "^0.1.2", - "@remix-run/react": "*", + "@babel/preset-typescript": "^7.28.5", "@tailwindcss/postcss": "^4.2.4", "@types/babel__core": "^7.20.5", "@types/prompts": "^2.4.9", "@types/react": "^19.2.14", "autoprefixer": "^10.5.0", + "babel-preset-solid": "^1.9.12", "clsx": "^2.1.1", - "es-module-lexer": "^2.1.0", "esbuild": "^0.28.0", - "next": "*", "postcss": "^8.5.13", "postcss-cli": "^11.0.0", "publint": "^0.3.18", @@ -240,8 +237,7 @@ "react-dom": "*", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", - "terser": "^5.46.2", - "tsup": "^8.5.1", + "typescript": "6.0.3", "vitest": "^3.0.0" }, "peerDependencies": { diff --git a/packages/scan/scripts/finalize-declarations.mjs b/packages/scan/scripts/finalize-declarations.mjs new file mode 100644 index 00000000..9ea90fc1 --- /dev/null +++ b/packages/scan/scripts/finalize-declarations.mjs @@ -0,0 +1,39 @@ +import { readdir, readFile, rename, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const DIST_PATH = new URL("../dist/", import.meta.url); + +const getDeclarationFiles = async (directory) => { + const entries = await readdir(directory, { withFileTypes: true }); + const files = await Promise.all( + entries.map((entry) => { + const entryPath = path.join(directory, entry.name); + return entry.isDirectory() ? getDeclarationFiles(entryPath) : entryPath; + }), + ); + return files.flat().filter((filePath) => /\.d\.(?:m)?ts$/.test(filePath)); +}; + +const finalizeDeclarationFormat = async (declarationExtension, runtimeExtension) => { + const internalDeclarationPath = new URL(`index${declarationExtension}`, DIST_PATH); + const publicDeclarationPath = new URL(`index2${declarationExtension}`, DIST_PATH); + const finalizedInternalPath = new URL(`internal-index${declarationExtension}`, DIST_PATH); + const declarationFiles = await getDeclarationFiles(DIST_PATH.pathname); + const importPattern = new RegExp(`((?:\\.\\./)+|\\./)index\\${runtimeExtension}`, "g"); + + await Promise.all( + declarationFiles.map(async (filePath) => { + const source = await readFile(filePath, "utf8"); + const nextSource = source.replace(importPattern, `$1internal-index${runtimeExtension}`); + if (nextSource !== source) { + await writeFile(filePath, nextSource); + } + }), + ); + + await rename(internalDeclarationPath, finalizedInternalPath); + await rename(publicDeclarationPath, internalDeclarationPath); +}; + +await finalizeDeclarationFormat(".d.ts", ".js"); +await finalizeDeclarationFormat(".d.mts", ".mjs"); diff --git a/packages/scan/solid-vite-plugin.ts b/packages/scan/solid-vite-plugin.ts new file mode 100644 index 00000000..7e2e6a47 --- /dev/null +++ b/packages/scan/solid-vite-plugin.ts @@ -0,0 +1,137 @@ +import * as babel from "@babel/core"; +import { build } from "esbuild"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { Plugin } from "vite-plus"; + +const CSS_DEFAULT_IMPORT_PATTERN = /import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+\.css)["'];?/g; +const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url)); +const OFFSCREEN_CANVAS_WORKER_PATH = resolve( + PACKAGE_ROOT, + "src/new-outlines/offscreen-canvas.worker.ts", +); +const NEW_OUTLINES_PATH = resolve(PACKAGE_ROOT, "src/new-outlines/index.ts"); +const WORKER_CODE_DECLARATION = 'const workerCode = "__WORKER_CODE__";'; + +let workerCodePromise: Promise | undefined; + +const compileWorkerCode = async (): Promise => { + const result = await build({ + entryPoints: [OFFSCREEN_CANVAS_WORKER_PATH], + bundle: true, + write: false, + format: "iife", + platform: "browser", + target: "es2019", + minify: true, + }); + const outputFile = result.outputFiles[0]; + if (!outputFile) { + throw new Error("Failed to compile the offscreen canvas worker"); + } + return outputFile.text; +}; + +export const cssTextPlugin = (): Plugin => ({ + name: "css-text", + enforce: "pre", + transform(source, id) { + const importerPath = id.split("?")[0]; + const transformedSource = source.replace( + CSS_DEFAULT_IMPORT_PATTERN, + (statement, variableName: string, cssPath: string) => { + if (!cssPath.startsWith(".") && !cssPath.startsWith("/")) return statement; + const resolvedPath = cssPath.startsWith("/") + ? cssPath + : resolve(dirname(importerPath), cssPath); + return `const ${variableName} = ${JSON.stringify(readFileSync(resolvedPath, "utf8"))};`; + }, + ); + if (transformedSource === source) return; + return { + code: transformedSource, + map: null, + }; + }, +}); + +export const solidWebBrowserPlugin = (): Plugin => { + const require = createRequire(import.meta.url); + const serverPath = require.resolve("solid-js/web"); + const browserPath = resolve(dirname(serverPath), "web.js"); + + return { + name: "solid-web-browser", + enforce: "pre" as const, + resolveId(source: string) { + if (source === "solid-js/web") return browserPath; + }, + }; +}; + +export const solidVitePlugin = (): Plugin => ({ + name: "solid-babel", + transform(source: string, id: string) { + if (!/\.(tsx|jsx)$/.test(id)) return; + + const result = babel.transformSync(source, { + presets: [ + ["@babel/preset-typescript", { onlyRemoveTypeImports: true }], + "babel-preset-solid", + ], + filename: id, + sourceMaps: true, + caller: { + name: "solid-babel", + supportsStaticESM: true, + }, + }); + + if (!result?.code) return; + return { + code: result.code, + map: result.map, + }; + }, +}); + +export const workerCodePlugin = (): Plugin => { + let workerCode = ""; + + return { + name: "offscreen-canvas-worker-code", + async buildStart() { + workerCodePromise ??= compileWorkerCode(); + workerCode = await workerCodePromise; + }, + transform(source, id) { + if (id.split("?")[0] !== NEW_OUTLINES_PATH) return; + if (!source.includes(WORKER_CODE_DECLARATION)) { + this.error(`Missing worker code declaration in ${id}`); + } + return { + code: source.replace( + WORKER_CODE_DECLARATION, + `const workerCode = ${JSON.stringify(workerCode)};`, + ), + map: null, + }; + }, + }; +}; + +export const declarationsOnlyPlugin = (): Plugin => ({ + name: "declarations-only", + generateBundle: { + order: "post", + handler(_outputOptions, bundle) { + for (const fileName of Object.keys(bundle)) { + if (!fileName.endsWith(".d.ts") && !fileName.endsWith(".d.mts")) { + delete bundle[fileName]; + } + } + }, + }, +}); diff --git a/packages/scan/src/auto.ts b/packages/scan/src/auto.ts index d2251b14..c975856a 100644 --- a/packages/scan/src/auto.ts +++ b/packages/scan/src/auto.ts @@ -1,13 +1,13 @@ -import './polyfills'; +import "./polyfills"; // Prioritize bippy side-effect -import 'bippy'; +import "bippy"; -import { IS_CLIENT } from '~web/utils/constants'; -import { scan } from './index'; +import { IS_CLIENT } from "./utils/is-client"; +import { scan } from "./index"; if (IS_CLIENT) { scan(); window.reactScan = scan; } -export * from './core'; +export * from "./core"; diff --git a/packages/scan/src/cli-utils.mts b/packages/scan/src/cli-utils.mts index 3b64b6df..78096d7f 100644 --- a/packages/scan/src/cli-utils.mts +++ b/packages/scan/src/cli-utils.mts @@ -1,9 +1,9 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; -type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun'; -type Framework = 'next' | 'vite' | 'tanstack' | 'webpack' | 'unknown'; -type NextRouterType = 'app' | 'pages' | 'unknown'; +type PackageManager = "npm" | "yarn" | "pnpm" | "bun"; +type Framework = "next" | "vite" | "tanstack" | "webpack" | "unknown"; +type NextRouterType = "app" | "pages" | "unknown"; interface ProjectInfo { packageManager: PackageManager; @@ -23,28 +23,29 @@ interface TransformResult { } interface DiffLine { - type: 'added' | 'removed' | 'unchanged'; + type: "added" | "removed" | "unchanged"; content: string; } const FRAMEWORK_NAMES: Record = { - next: 'Next.js', - vite: 'Vite', - tanstack: 'TanStack Start', - webpack: 'Webpack', - unknown: 'Unknown', + next: "Next.js", + vite: "Vite", + tanstack: "TanStack Start", + webpack: "Webpack", + unknown: "Unknown", }; const INSTALL_COMMANDS: Record = { - npm: 'npm install -D', - yarn: 'yarn add -D', - pnpm: 'pnpm add -D', - bun: 'bun add -D', + npm: "npm install -D", + yarn: "yarn add -D", + pnpm: "pnpm add -D", + bun: "bun add -D", }; // --- Templates --- -const REACT_SCAN_SCRIPT_TAG = ''; +const REACT_SCAN_SCRIPT_TAG = + ''; const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && ( ')).toBe(true); + it("detects react-scan in script tag", () => { + expect( + hasReactScanCode(''), + ).toBe(true); }); }); // --- findLayoutFile --- -describe('findLayoutFile', () => { - it('finds app/layout.tsx for app router', () => { - mkdirSync(join(tempDirectory, 'app')); - const layoutPath = join(tempDirectory, 'app', 'layout.tsx'); - writeFileSync(layoutPath, ''); - expect(findLayoutFile(tempDirectory, 'app')).toBe(layoutPath); +describe("findLayoutFile", () => { + it("finds app/layout.tsx for app router", () => { + mkdirSync(join(tempDirectory, "app")); + const layoutPath = join(tempDirectory, "app", "layout.tsx"); + writeFileSync(layoutPath, ""); + expect(findLayoutFile(tempDirectory, "app")).toBe(layoutPath); }); - it('finds src/app/layout.tsx for app router', () => { - mkdirSync(join(tempDirectory, 'src', 'app'), { recursive: true }); - const layoutPath = join(tempDirectory, 'src', 'app', 'layout.tsx'); - writeFileSync(layoutPath, ''); - expect(findLayoutFile(tempDirectory, 'app')).toBe(layoutPath); + it("finds src/app/layout.tsx for app router", () => { + mkdirSync(join(tempDirectory, "src", "app"), { recursive: true }); + const layoutPath = join(tempDirectory, "src", "app", "layout.tsx"); + writeFileSync(layoutPath, ""); + expect(findLayoutFile(tempDirectory, "app")).toBe(layoutPath); }); - it('finds app/layout.jsx for app router', () => { - mkdirSync(join(tempDirectory, 'app')); - const layoutPath = join(tempDirectory, 'app', 'layout.jsx'); - writeFileSync(layoutPath, ''); - expect(findLayoutFile(tempDirectory, 'app')).toBe(layoutPath); + it("finds app/layout.jsx for app router", () => { + mkdirSync(join(tempDirectory, "app")); + const layoutPath = join(tempDirectory, "app", "layout.jsx"); + writeFileSync(layoutPath, ""); + expect(findLayoutFile(tempDirectory, "app")).toBe(layoutPath); }); - it('finds pages/_document.tsx for pages router', () => { - mkdirSync(join(tempDirectory, 'pages')); - const documentPath = join(tempDirectory, 'pages', '_document.tsx'); - writeFileSync(documentPath, ''); - expect(findLayoutFile(tempDirectory, 'pages')).toBe(documentPath); + it("finds pages/_document.tsx for pages router", () => { + mkdirSync(join(tempDirectory, "pages")); + const documentPath = join(tempDirectory, "pages", "_document.tsx"); + writeFileSync(documentPath, ""); + expect(findLayoutFile(tempDirectory, "pages")).toBe(documentPath); }); - it('finds src/pages/_document.tsx for pages router', () => { - mkdirSync(join(tempDirectory, 'src', 'pages'), { recursive: true }); - const documentPath = join(tempDirectory, 'src', 'pages', '_document.tsx'); - writeFileSync(documentPath, ''); - expect(findLayoutFile(tempDirectory, 'pages')).toBe(documentPath); + it("finds src/pages/_document.tsx for pages router", () => { + mkdirSync(join(tempDirectory, "src", "pages"), { recursive: true }); + const documentPath = join(tempDirectory, "src", "pages", "_document.tsx"); + writeFileSync(documentPath, ""); + expect(findLayoutFile(tempDirectory, "pages")).toBe(documentPath); }); - it('returns null when no layout file exists for app router', () => { - expect(findLayoutFile(tempDirectory, 'app')).toBeNull(); + it("returns null when no layout file exists for app router", () => { + expect(findLayoutFile(tempDirectory, "app")).toBeNull(); }); - it('returns null when no document file exists for pages router', () => { - expect(findLayoutFile(tempDirectory, 'pages')).toBeNull(); + it("returns null when no document file exists for pages router", () => { + expect(findLayoutFile(tempDirectory, "pages")).toBeNull(); }); - it('returns null for unknown router type', () => { - expect(findLayoutFile(tempDirectory, 'unknown')).toBeNull(); + it("returns null for unknown router type", () => { + expect(findLayoutFile(tempDirectory, "unknown")).toBeNull(); }); }); // --- findIndexHtml --- -describe('findIndexHtml', () => { - it('finds root index.html', () => { - const indexPath = join(tempDirectory, 'index.html'); - writeFileSync(indexPath, ''); +describe("findIndexHtml", () => { + it("finds root index.html", () => { + const indexPath = join(tempDirectory, "index.html"); + writeFileSync(indexPath, ""); expect(findIndexHtml(tempDirectory)).toBe(indexPath); }); - it('finds public/index.html', () => { - mkdirSync(join(tempDirectory, 'public')); - const indexPath = join(tempDirectory, 'public', 'index.html'); - writeFileSync(indexPath, ''); + it("finds public/index.html", () => { + mkdirSync(join(tempDirectory, "public")); + const indexPath = join(tempDirectory, "public", "index.html"); + writeFileSync(indexPath, ""); expect(findIndexHtml(tempDirectory)).toBe(indexPath); }); - it('finds src/index.html', () => { - mkdirSync(join(tempDirectory, 'src')); - const indexPath = join(tempDirectory, 'src', 'index.html'); - writeFileSync(indexPath, ''); + it("finds src/index.html", () => { + mkdirSync(join(tempDirectory, "src")); + const indexPath = join(tempDirectory, "src", "index.html"); + writeFileSync(indexPath, ""); expect(findIndexHtml(tempDirectory)).toBe(indexPath); }); - it('prefers root index.html over public/index.html', () => { - const rootPath = join(tempDirectory, 'index.html'); - writeFileSync(rootPath, ''); - mkdirSync(join(tempDirectory, 'public')); - writeFileSync(join(tempDirectory, 'public', 'index.html'), ''); + it("prefers root index.html over public/index.html", () => { + const rootPath = join(tempDirectory, "index.html"); + writeFileSync(rootPath, ""); + mkdirSync(join(tempDirectory, "public")); + writeFileSync(join(tempDirectory, "public", "index.html"), ""); expect(findIndexHtml(tempDirectory)).toBe(rootPath); }); - it('returns null when no index.html exists', () => { + it("returns null when no index.html exists", () => { expect(findIndexHtml(tempDirectory)).toBeNull(); }); }); // --- findEntryFile --- -describe('findEntryFile', () => { - it('finds src/index.tsx', () => { - mkdirSync(join(tempDirectory, 'src')); - const entryPath = join(tempDirectory, 'src', 'index.tsx'); - writeFileSync(entryPath, ''); +describe("findEntryFile", () => { + it("finds src/index.tsx", () => { + mkdirSync(join(tempDirectory, "src")); + const entryPath = join(tempDirectory, "src", "index.tsx"); + writeFileSync(entryPath, ""); expect(findEntryFile(tempDirectory)).toBe(entryPath); }); - it('finds src/main.tsx', () => { - mkdirSync(join(tempDirectory, 'src')); - const entryPath = join(tempDirectory, 'src', 'main.tsx'); - writeFileSync(entryPath, ''); + it("finds src/main.tsx", () => { + mkdirSync(join(tempDirectory, "src")); + const entryPath = join(tempDirectory, "src", "main.tsx"); + writeFileSync(entryPath, ""); expect(findEntryFile(tempDirectory)).toBe(entryPath); }); - it('finds src/index.js', () => { - mkdirSync(join(tempDirectory, 'src')); - const entryPath = join(tempDirectory, 'src', 'index.js'); - writeFileSync(entryPath, ''); + it("finds src/index.js", () => { + mkdirSync(join(tempDirectory, "src")); + const entryPath = join(tempDirectory, "src", "index.js"); + writeFileSync(entryPath, ""); expect(findEntryFile(tempDirectory)).toBe(entryPath); }); - it('prefers src/index.tsx over src/main.tsx', () => { - mkdirSync(join(tempDirectory, 'src')); - const indexPath = join(tempDirectory, 'src', 'index.tsx'); - writeFileSync(indexPath, ''); - writeFileSync(join(tempDirectory, 'src', 'main.tsx'), ''); + it("prefers src/index.tsx over src/main.tsx", () => { + mkdirSync(join(tempDirectory, "src")); + const indexPath = join(tempDirectory, "src", "index.tsx"); + writeFileSync(indexPath, ""); + writeFileSync(join(tempDirectory, "src", "main.tsx"), ""); expect(findEntryFile(tempDirectory)).toBe(indexPath); }); - it('returns null when no entry file exists', () => { + it("returns null when no entry file exists", () => { expect(findEntryFile(tempDirectory)).toBeNull(); }); }); // --- transformNextAppRouter --- -describe('transformNextAppRouter', () => { +describe("transformNextAppRouter", () => { const LAYOUT_WITH_BODY = `export default function RootLayout({ children }) { return ( @@ -363,56 +360,47 @@ describe('transformNextAppRouter', () => { ); }`; - const LAYOUT_WITH_HEAD = `export default function RootLayout({ children }) { - return ( - - App - {children} - - ); -}`; - - it('returns failure when no layout file exists', () => { - const result = transformNextAppRouter(tempDirectory, 'app'); + it("returns failure when no layout file exists", () => { + const result = transformNextAppRouter(tempDirectory, "app"); expect(result.success).toBe(false); - expect(result.message).toContain('Could not find'); + expect(result.message).toContain("Could not find"); }); - it('injects script after body tag when no head tag exists', () => { - mkdirSync(join(tempDirectory, 'app')); - writeFileSync(join(tempDirectory, 'app', 'layout.tsx'), LAYOUT_WITH_BODY); + it("injects script after body tag when no head tag exists", () => { + mkdirSync(join(tempDirectory, "app")); + writeFileSync(join(tempDirectory, "app", "layout.tsx"), LAYOUT_WITH_BODY); - const result = transformNextAppRouter(tempDirectory, 'app'); + const result = transformNextAppRouter(tempDirectory, "app"); expect(result.success).toBe(true); - expect(result.newContent).toContain('react-scan'); - expect(result.newContent).toContain(''); + expect(result.newContent).toContain("react-scan"); + expect(result.newContent).toContain(""); }); - it('reports already installed when react-scan is in content', () => { - mkdirSync(join(tempDirectory, 'app')); + it("reports already installed when react-scan is in content", () => { + mkdirSync(join(tempDirectory, "app")); writeFileSync( - join(tempDirectory, 'app', 'layout.tsx'), + join(tempDirectory, "app", "layout.tsx"), 'import "react-scan";\n' + LAYOUT_WITH_BODY, ); - const result = transformNextAppRouter(tempDirectory, 'app'); + const result = transformNextAppRouter(tempDirectory, "app"); expect(result.success).toBe(true); expect(result.noChanges).toBe(true); - expect(result.message).toContain('already installed'); + expect(result.message).toContain("already installed"); }); - it('preserves original content', () => { - mkdirSync(join(tempDirectory, 'app')); - writeFileSync(join(tempDirectory, 'app', 'layout.tsx'), LAYOUT_WITH_BODY); + it("preserves original content", () => { + mkdirSync(join(tempDirectory, "app")); + writeFileSync(join(tempDirectory, "app", "layout.tsx"), LAYOUT_WITH_BODY); - const result = transformNextAppRouter(tempDirectory, 'app'); + const result = transformNextAppRouter(tempDirectory, "app"); expect(result.originalContent).toBe(LAYOUT_WITH_BODY); }); }); // --- transformNextPagesRouter --- -describe('transformNextPagesRouter', () => { +describe("transformNextPagesRouter", () => { const DOCUMENT_WITH_HEAD = `import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { @@ -427,30 +415,30 @@ export default function Document() { ); }`; - it('returns failure when no _document file exists', () => { - const result = transformNextPagesRouter(tempDirectory, 'pages'); + it("returns failure when no _document file exists", () => { + const result = transformNextPagesRouter(tempDirectory, "pages"); expect(result.success).toBe(false); - expect(result.message).toContain('Could not find'); + expect(result.message).toContain("Could not find"); }); - it('injects script inside Head tag', () => { - mkdirSync(join(tempDirectory, 'pages')); - writeFileSync(join(tempDirectory, 'pages', '_document.tsx'), DOCUMENT_WITH_HEAD); + it("injects script inside Head tag", () => { + mkdirSync(join(tempDirectory, "pages")); + writeFileSync(join(tempDirectory, "pages", "_document.tsx"), DOCUMENT_WITH_HEAD); - const result = transformNextPagesRouter(tempDirectory, 'pages'); + const result = transformNextPagesRouter(tempDirectory, "pages"); expect(result.success).toBe(true); - expect(result.newContent).toContain('react-scan'); - expect(result.newContent).toContain(''); + expect(result.newContent).toContain("react-scan"); + expect(result.newContent).toContain(""); }); - it('reports already installed when react-scan is in content', () => { - mkdirSync(join(tempDirectory, 'pages')); + it("reports already installed when react-scan is in content", () => { + mkdirSync(join(tempDirectory, "pages")); writeFileSync( - join(tempDirectory, 'pages', '_document.tsx'), - DOCUMENT_WITH_HEAD.replace('', '\n'; const diff = generateDiff(original, updated); - const addedLines = diff.filter((diffLine) => diffLine.type === 'added'); + const addedLines = diff.filter((diffLine) => diffLine.type === "added"); expect(addedLines.length).toBe(1); - expect(addedLines[0].content).toContain('react-scan'); + expect(addedLines[0].content).toContain("react-scan"); }); }); diff --git a/packages/scan/src/cli.mts b/packages/scan/src/cli.mts index 785ded34..acfb08b9 100644 --- a/packages/scan/src/cli.mts +++ b/packages/scan/src/cli.mts @@ -1,9 +1,9 @@ -import { execSync } from 'node:child_process'; -import { existsSync, writeFileSync } from 'node:fs'; -import { join, relative, resolve } from 'node:path'; -import { Command } from 'commander'; -import pc from 'picocolors'; -import prompts from 'prompts'; +import { execSync } from "node:child_process"; +import { existsSync, writeFileSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import { Command } from "commander"; +import pc from "picocolors"; +import prompts from "prompts"; import { type DiffLine, type PackageManager, @@ -12,9 +12,9 @@ import { detectProject, generateDiff, previewTransform, -} from './cli-utils.mjs'; +} from "./cli-utils.mjs"; -const VERSION = process.env.NPM_PACKAGE_VERSION ?? '0.0.0'; +const VERSION = process.env.NPM_PACKAGE_VERSION ?? "0.0.0"; // --- Diff --- @@ -22,16 +22,16 @@ const printDiff = (filePath: string, original: string, updated: string): void => const diff = generateDiff(original, updated); const contextLines = 3; const changedIndices = diff - .map((line: DiffLine, i: number) => (line.type !== 'unchanged' ? i : -1)) + .map((line: DiffLine, i: number) => (line.type !== "unchanged" ? i : -1)) .filter((i: number) => i !== -1); if (changedIndices.length === 0) { - console.log(pc.dim(' No changes')); + console.log(pc.dim(" No changes")); return; } console.log(`\n${pc.bold(`File: ${filePath}`)}`); - console.log(pc.dim('─'.repeat(60))); + console.log(pc.dim("─".repeat(60))); let lastPrintedIdx = -1; @@ -40,14 +40,14 @@ const printDiff = (filePath: string, original: string, updated: string): void => const end = Math.min(diff.length - 1, changedIdx + contextLines); if (start > lastPrintedIdx + 1 && lastPrintedIdx !== -1) { - console.log(pc.dim(' ...')); + console.log(pc.dim(" ...")); } for (let i = Math.max(start, lastPrintedIdx + 1); i <= end; i++) { const line = diff[i]; - if (line.type === 'added') { + if (line.type === "added") { console.log(pc.green(`+ ${line.content}`)); - } else if (line.type === 'removed') { + } else if (line.type === "removed") { console.log(pc.red(`- ${line.content}`)); } else { console.log(pc.dim(` ${line.content}`)); @@ -56,7 +56,7 @@ const printDiff = (filePath: string, original: string, updated: string): void => } } - console.log(pc.dim('─'.repeat(60))); + console.log(pc.dim("─".repeat(60))); }; // --- Install --- @@ -68,30 +68,27 @@ const installPackages = ( ): void => { if (packages.length === 0) return; - const command = `${INSTALL_COMMANDS[packageManager]} ${packages.join(' ')}`; + const command = `${INSTALL_COMMANDS[packageManager]} ${packages.join(" ")}`; console.log(pc.dim(` Running: ${command}\n`)); execSync(command, { cwd: projectRoot, - stdio: 'inherit', + stdio: "inherit", }); }; // --- Main --- -const program = new Command() - .name('react-scan') - .description('React Scan CLI') - .version(VERSION); +const program = new Command().name("react-scan").description("React Scan CLI").version(VERSION); program - .command('init') - .description('Set up React Scan in your project') - .option('-y, --yes', 'skip confirmation prompts', false) - .option('-c, --cwd ', 'working directory', process.cwd()) - .option('--skip-install', 'skip package installation', false) + .command("init") + .description("Set up React Scan in your project") + .option("-y, --yes", "skip confirmation prompts", false) + .option("-c, --cwd ", "working directory", process.cwd()) + .option("--skip-install", "skip package installation", false) .action(async (opts) => { - console.log(`\n${pc.magenta('[·]')} ${pc.bold('React Scan')} ${pc.dim(`v${VERSION}`)}\n`); + console.log(`\n${pc.magenta("[·]")} ${pc.bold("React Scan")} ${pc.dim(`v${VERSION}`)}\n`); try { const cwd = resolve(opts.cwd); @@ -101,32 +98,36 @@ program process.exit(1); } - if (!existsSync(join(cwd, 'package.json'))) { - console.error(pc.red('No package.json found. Run this command from a project root.')); + if (!existsSync(join(cwd, "package.json"))) { + console.error(pc.red("No package.json found. Run this command from a project root.")); process.exit(1); } - console.log(pc.dim(' Detecting project...\n')); + console.log(pc.dim(" Detecting project...\n")); const project = detectProject(cwd); - if (project.framework === 'unknown') { - console.error(pc.red(' Could not detect a supported framework.')); - console.log(pc.dim(' React Scan supports Next.js, Vite, and Webpack projects.')); - console.log(pc.dim(' Visit https://github.com/aidenybai/react-scan#install for manual setup.\n')); + if (project.framework === "unknown") { + console.error(pc.red(" Could not detect a supported framework.")); + console.log(pc.dim(" React Scan supports Next.js, Vite, and Webpack projects.")); + console.log( + pc.dim(" Visit https://github.com/aidenybai/react-scan#install for manual setup.\n"), + ); process.exit(1); } console.log(` Framework: ${pc.cyan(FRAMEWORK_NAMES[project.framework])}`); - if (project.framework === 'next') { - console.log(` Router: ${pc.cyan(project.nextRouterType === 'app' ? 'App Router' : 'Pages Router')}`); + if (project.framework === "next") { + console.log( + ` Router: ${pc.cyan(project.nextRouterType === "app" ? "App Router" : "Pages Router")}`, + ); } console.log(` Package manager: ${pc.cyan(project.packageManager)}`); console.log(); if (project.hasReactScan) { - console.log(pc.green(' React Scan is already installed in package.json.')); - console.log(pc.dim(' Checking if code setup is needed...\n')); + console.log(pc.green(" React Scan is already installed in package.json.")); + console.log(pc.dim(" Checking if code setup is needed...\n")); } const result = previewTransform(cwd, project.framework, project.nextRouterType); @@ -139,71 +140,69 @@ program const hasCodeChanges = !result.noChanges && result.originalContent && result.newContent; if (hasCodeChanges) { - printDiff( - relative(cwd, result.filePath), - result.originalContent!, - result.newContent!, - ); + printDiff(relative(cwd, result.filePath), result.originalContent!, result.newContent!); console.log(); - console.log(pc.yellow(' Auto-detection may not be 100% accurate.')); - console.log(pc.yellow(' Please verify the changes before committing.\n')); + console.log(pc.yellow(" Auto-detection may not be 100% accurate.")); + console.log(pc.yellow(" Please verify the changes before committing.\n")); if (!opts.yes) { const { proceed } = await prompts({ - type: 'confirm', - name: 'proceed', - message: 'Apply these changes?', + type: "confirm", + name: "proceed", + message: "Apply these changes?", initial: true, }); if (!proceed) { - console.log(pc.dim('\n Changes cancelled.\n')); + console.log(pc.dim("\n Changes cancelled.\n")); process.exit(0); } } } if (!opts.skipInstall && !project.hasReactScan) { - console.log(pc.dim('\n Installing react-scan...\n')); - installPackages(['react-scan'], project.packageManager, cwd); + console.log(pc.dim("\n Installing react-scan...\n")); + installPackages(["react-scan"], project.packageManager, cwd); console.log(); } if (hasCodeChanges) { - writeFileSync(result.filePath, result.newContent!, 'utf-8'); + writeFileSync(result.filePath, result.newContent!, "utf-8"); console.log(pc.green(` Updated ${relative(cwd, result.filePath)}`)); } if (!hasCodeChanges && project.hasReactScan) { - console.log(pc.green(' React Scan is already set up in your project.\n')); + console.log(pc.green(" React Scan is already set up in your project.\n")); process.exit(0); } const { runDoctor } = await prompts({ - type: 'confirm', - name: 'runDoctor', - message: 'Install React Doctor?', + type: "confirm", + name: "runDoctor", + message: "Install React Doctor?", initial: true, }); if (runDoctor) { try { - console.log(pc.dim('\n Installing React Doctor...\n')); - execSync('npx -y react-doctor@latest install --yes', { + console.log(pc.dim("\n Installing React Doctor...\n")); + execSync("npx -y react-doctor@latest install --yes", { cwd, - stdio: 'inherit', + stdio: "inherit", }); } catch { - console.log(pc.dim('\n React Doctor installation skipped.\n')); + console.log(pc.dim("\n React Doctor installation skipped.\n")); } } console.log(); - console.log(`${pc.green(' Success!')} React Scan has been installed.`); - console.log(pc.dim(' You may now start your development server.\n')); + console.log(`${pc.green(" Success!")} React Scan has been installed.`); + console.log(pc.dim(" You may now start your development server.\n")); } catch (error) { - console.error(pc.red(`\n Error: ${error instanceof Error ? error.message : String(error)}\n`)); + console.error( + pc.red(`\n Error: ${error instanceof Error ? error.message : String(error)}\n`), + ); process.exit(1); } }); diff --git a/packages/scan/src/core/all-environments.ts b/packages/scan/src/core/all-environments.ts index df7d9d0b..6ae32d77 100644 --- a/packages/scan/src/core/all-environments.ts +++ b/packages/scan/src/core/all-environments.ts @@ -1,7 +1,7 @@ -import { ReactScanInternals, scan as innerScan } from '.'; +import { ReactScanInternals, scan as innerScan } from "."; export const scan = /*#__PURE__*/ (...params: Parameters) => { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { ReactScanInternals.runInAllEnvironments = true; innerScan(...params); } diff --git a/packages/scan/src/core/compatibility.ts b/packages/scan/src/core/compatibility.ts new file mode 100644 index 00000000..98a29f86 --- /dev/null +++ b/packages/scan/src/core/compatibility.ts @@ -0,0 +1,48 @@ +import { createSignal, type Accessor } from "solid-js"; + +export interface ReadonlySignal { + readonly value: Value; +} + +export interface Signal extends ReadonlySignal { + value: Value; + subscribe: (listener: (value: Value) => void) => () => void; +} + +export interface CompatibleSignal { + get: Accessor; + set: (value: Value) => Value; + facade: Signal; +} + +export const createCompatibleSignal = (initialValue: Value): CompatibleSignal => { + const [get, setValue] = createSignal(initialValue); + const listeners = new Set<(value: Value) => void>(); + + const set = (value: Value): Value => { + const previousValue = get(); + if (Object.is(previousValue, value)) return previousValue; + setValue(() => value); + listeners.forEach((listener) => listener(value)); + return value; + }; + + return { + get, + set, + facade: { + get value() { + return get(); + }, + set value(nextValue: Value) { + set(nextValue); + }, + subscribe: (listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + }, + }; +}; diff --git a/packages/scan/src/core/fast-serialize.test.ts b/packages/scan/src/core/fast-serialize.test.ts index a7e6f346..69da6889 100644 --- a/packages/scan/src/core/fast-serialize.test.ts +++ b/packages/scan/src/core/fast-serialize.test.ts @@ -1,61 +1,61 @@ -import { describe, expect, it } from 'vitest'; -import { fastSerialize } from '~core/instrumentation'; +import { describe, expect, it } from "vitest"; +import { fastSerialize } from "./instrumentation"; -describe('fastSerialize', () => { - it('serializes null', () => { - expect(fastSerialize(null)).toBe('null'); +describe("fastSerialize", () => { + it("serializes null", () => { + expect(fastSerialize(null)).toBe("null"); }); - it('serializes undefined', () => { - expect(fastSerialize(undefined)).toBe('undefined'); + it("serializes undefined", () => { + expect(fastSerialize(undefined)).toBe("undefined"); }); - it('serializes strings', () => { - expect(fastSerialize('hello')).toBe('hello'); - expect(fastSerialize('')).toBe(''); + it("serializes strings", () => { + expect(fastSerialize("hello")).toBe("hello"); + expect(fastSerialize("")).toBe(""); }); - it('serializes numbers', () => { - expect(fastSerialize(42)).toBe('42'); - expect(fastSerialize(0)).toBe('0'); - expect(fastSerialize(Number.NaN)).toBe('NaN'); + it("serializes numbers", () => { + expect(fastSerialize(42)).toBe("42"); + expect(fastSerialize(0)).toBe("0"); + expect(fastSerialize(Number.NaN)).toBe("NaN"); }); - it('serializes booleans', () => { - expect(fastSerialize(true)).toBe('true'); - expect(fastSerialize(false)).toBe('false'); + it("serializes booleans", () => { + expect(fastSerialize(true)).toBe("true"); + expect(fastSerialize(false)).toBe("false"); }); - it('serializes functions', () => { + it("serializes functions", () => { const testFunc = (_x: 2) => 3; - expect(fastSerialize(testFunc)).toBe('(_x) => 3'); + expect(fastSerialize(testFunc)).toBe("(_x) => 3"); }); - it('serializes arrays', () => { - expect(fastSerialize([])).toBe('[]'); - expect(fastSerialize([1, 2, 3])).toBe('[3]'); + it("serializes arrays", () => { + expect(fastSerialize([])).toBe("[]"); + expect(fastSerialize([1, 2, 3])).toBe("[3]"); }); - it('serializes plain objects', () => { - expect(fastSerialize({})).toBe('{}'); - expect(fastSerialize({ a: 1, b: 2 })).toBe('{2}'); + it("serializes plain objects", () => { + expect(fastSerialize({})).toBe("{}"); + expect(fastSerialize({ a: 1, b: 2 })).toBe("{2}"); }); - it('serializes deeply nested objects with depth limit', () => { + it("serializes deeply nested objects with depth limit", () => { const nested = { a: { b: { c: 1 } } }; - expect(fastSerialize(nested, 0)).toBe('{1}'); - expect(fastSerialize(nested, -1)).toBe('…'); + expect(fastSerialize(nested, 0)).toBe("{1}"); + expect(fastSerialize(nested, -1)).toBe("…"); }); - it('serializes objects with custom constructors', () => { + it("serializes objects with custom constructors", () => { class CustomClass {} const instance = new CustomClass(); - expect(fastSerialize(instance)).toBe('CustomClass{…}'); + expect(fastSerialize(instance)).toBe("CustomClass{…}"); }); - it('serializes unknown objects gracefully', () => { + it("serializes unknown objects gracefully", () => { const date = new Date(); const serialized = fastSerialize(date); - expect(serialized.includes('Date')).toBe(true); + expect(serialized.includes("Date")).toBe(true); }); }); diff --git a/packages/scan/src/core/get-is-production.test.ts b/packages/scan/src/core/get-is-production.test.ts index d56971e0..2e0a6902 100644 --- a/packages/scan/src/core/get-is-production.test.ts +++ b/packages/scan/src/core/get-is-production.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from "vitest"; interface FakeRenderer { bundleType?: number; @@ -11,10 +11,10 @@ interface FakeRDTHook { const sharedHook: FakeRDTHook = { renderers: new Map() }; -vi.mock('bippy', () => { +vi.mock("bippy", () => { return { detectReactBuildType: (renderer: FakeRenderer) => - renderer.bundleType === 0 ? 'production' : 'development', + renderer.bundleType === 0 ? "production" : "development", getRDTHook: () => sharedHook, getType: () => null, isInstrumentationActive: () => false, @@ -23,7 +23,7 @@ vi.mock('bippy', () => { const importFreshGetIsProduction = async () => { vi.resetModules(); - const mod = (await import('~core/index')) as typeof import('~core/index'); + const mod = (await import("./index")) as typeof import("./index"); return mod.getIsProduction; }; @@ -31,26 +31,26 @@ beforeEach(() => { sharedHook.renderers.clear(); }); -describe('getIsProduction', () => { - it('returns null when no React renderer has registered yet', async () => { +describe("getIsProduction", () => { + it("returns null when no React renderer has registered yet", async () => { const getIsProduction = await importFreshGetIsProduction(); expect(getIsProduction()).toBeNull(); }); - it('returns true when every registered renderer is production', async () => { + it("returns true when every registered renderer is production", async () => { const getIsProduction = await importFreshGetIsProduction(); sharedHook.renderers.set(1, { bundleType: 0 }); expect(getIsProduction()).toBe(true); }); - it('returns false when at least one renderer is non-production', async () => { + it("returns false when at least one renderer is non-production", async () => { const getIsProduction = await importFreshGetIsProduction(); sharedHook.renderers.set(1, { bundleType: 0 }); sharedHook.renderers.set(2, { bundleType: 1 }); expect(getIsProduction()).toBe(false); }); - it('does not cache `true` — a later dev renderer flips the result', async () => { + it("does not cache `true` — a later dev renderer flips the result", async () => { // Regression guard for #402: the Next.js dev overlay registers a // production React build first, then the user's dev React arrives a // tick later. Caching `true` on the first call would lock us out. @@ -62,7 +62,7 @@ describe('getIsProduction', () => { expect(getIsProduction()).toBe(false); }); - it('caches `false` permanently once a dev renderer has been seen', async () => { + it("caches `false` permanently once a dev renderer has been seen", async () => { const getIsProduction = await importFreshGetIsProduction(); sharedHook.renderers.set(1, { bundleType: 1 }); expect(getIsProduction()).toBe(false); diff --git a/packages/scan/src/core/index.test.ts b/packages/scan/src/core/index.test.ts new file mode 100644 index 00000000..02f9532d --- /dev/null +++ b/packages/scan/src/core/index.test.ts @@ -0,0 +1,64 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as packageExports from "../index"; +import * as rscExports from "../rsc-shim"; +import { ReactScanInternals, Store, getOptions, getReport, setOptions } from "./index"; + +const initialOptions = ReactScanInternals.options.value; +const initialInspectState = Store.inspectState.value; +const internalNativeExportNames = [ + "getInspectState", + "setInspectState", + "getLastReportTime", + "setLastReportTime", + "getIsInIframe", + "getOptionsState", + "setOptionsState", +]; + +afterEach(() => { + ReactScanInternals.options.value = initialOptions; + Store.inspectState.value = initialInspectState; +}); + +describe("public compatibility state", () => { + it("keeps getOptions and ReactScanInternals on the same signal facade", () => { + expect(getOptions()).toBe(ReactScanInternals.options); + }); + + it("notifies option subscribers after setOptions", () => { + const listener = vi.fn(); + const unsubscribe = ReactScanInternals.options.subscribe(listener); + + setOptions({ log: !initialOptions.log }); + + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ log: !initialOptions.log })); + unsubscribe(); + }); + + it("preserves writable Store signal semantics", () => { + const listener = vi.fn(); + const unsubscribe = Store.inspectState.subscribe(listener); + const nextState = { kind: "inspect-off" } as const; + + Store.inspectState.value = nextState; + + expect(Store.inspectState.value).toBe(nextState); + expect(listener).toHaveBeenCalledWith(nextState); + unsubscribe(); + }); + + it("returns the legacy report map for compatibility", () => { + expect(getReport()).toBe(Store.legacyReportData); + }); +}); + +describe("public package exports", () => { + it.each(internalNativeExportNames)("does not expose %s", (exportName) => { + expect(packageExports).not.toHaveProperty(exportName); + expect(rscExports).not.toHaveProperty(exportName); + }); + + it("keeps the server ignoredProps facade weak-set compatible", () => { + expect(rscExports.ignoredProps).toBeInstanceOf(WeakSet); + }); +}); diff --git a/packages/scan/src/core/index.ts b/packages/scan/src/core/index.ts index e5cc0580..2ba912d3 100644 --- a/packages/scan/src/core/index.ts +++ b/packages/scan/src/core/index.ts @@ -1,4 +1,6 @@ -import { type Signal, signal } from "@preact/signals"; +import type { Signal } from "./compatibility"; +import type {} from "../types"; +import { createScanRuntime } from "../runtime/create-scan-runtime"; import { type Fiber, detectReactBuildType, @@ -6,50 +8,25 @@ import { getType, isInstrumentationActive, } from "bippy"; -import type { ComponentType } from "preact"; -import type { ReactNode } from "preact/compat"; -import type { RenderData } from "src/core/utils"; -import { initReactScanInstrumentation } from "src/new-outlines"; -import styles from "~web/assets/css/styles.css"; -import { createToolbar } from "~web/toolbar"; -import { IS_CLIENT } from "~web/utils/constants"; -import { checkReactGrabVersion } from "~web/utils/check-react-grab-version"; -import { readLocalStorage, saveLocalStorage } from "~web/utils/helpers"; -import { parseSafeAreaOption } from "~web/utils/parse-safe-area-option"; -import type { States } from "~web/views/inspector/utils"; +import type { ComponentType, ReactNode } from "react"; +import type { RenderData } from "./utils"; +import { initReactScanInstrumentation } from "../new-outlines"; +import { checkReactGrabVersion } from "../utils/check-react-grab-version"; +import { IS_CLIENT } from "../utils/is-client"; +import { parseSafeAreaOption } from "../utils/parse-safe-area-option"; +import { readLocalStorage } from "../utils/read-local-storage"; +import { saveLocalStorage } from "../utils/save-local-storage"; +import type { States } from "./inspection/types"; import type { ChangeReason, Render, createInstrumentation } from "./instrumentation"; -import { startTimingTracking } from "./notifications/event-tracking"; -import { createHighlightCanvas } from "./notifications/outline-overlay"; +import { + Store as nativeStore, + getIsInIframe, + getOptionsState, + optionsFacade, + setOptionsState, +} from "./native-state"; import packageJson from "../../package.json"; -let rootContainer: HTMLDivElement | null = null; -let shadowRoot: ShadowRoot | null = null; - -interface RootContainer { - rootContainer: HTMLDivElement; - shadowRoot: ShadowRoot; -} - -const initRootContainer = (): RootContainer => { - if (rootContainer && shadowRoot) { - return { rootContainer, shadowRoot }; - } - - rootContainer = document.createElement("div"); - rootContainer.id = "react-scan-root"; - - shadowRoot = rootContainer.attachShadow({ mode: "open" }); - - const cssStyles = document.createElement("style"); - cssStyles.textContent = styles; - - shadowRoot.appendChild(cssStyles); - - document.documentElement.appendChild(rootContainer); - - return { rootContainer, shadowRoot }; -}; - export interface Options { /** * Enable/disable scanning @@ -230,33 +207,12 @@ export type ChangesPayload = { }; export type ChangesListener = (changes: ChangesPayload) => void; -export const Store: StoreType = { - wasDetailsOpen: signal(true), - isInIframe: signal(IS_CLIENT && window.self !== window.top), - inspectState: signal({ - kind: "uninitialized", - }), - fiberRoots: new Set(), - reportData: new Map(), - legacyReportData: new Map(), - lastReportTime: signal(0), - interactionListeningForRenders: null, - changesListeners: new Map(), -}; +export const Store: StoreType = nativeStore; export const ReactScanInternals: Internals = { instrumentation: null, componentAllowList: null, - options: signal({ - enabled: true, - log: false, - showToolbar: true, - animationSpeed: "fast", - dangerouslyForceRunInProduction: false, - showFPS: true, - showNotificationCount: true, - allowInIframe: false, - }), + options: optionsFacade, runInAllEnvironments: false, onRender: null, Store, @@ -270,7 +226,12 @@ if (IS_CLIENT && window.__REACT_SCAN_EXTENSION__) { export type LocalStorageOptions = Omit; const applyLocalStorageOptions = (options: Options): LocalStorageOptions => { - const { onCommitStart, onRender, onCommitFinish, ...rest } = options; + const { + onCommitStart: _onCommitStart, + onRender: _onRender, + onCommitFinish: _onCommitFinish, + ...rest + } = options; return rest; }; @@ -369,16 +330,16 @@ export const setOptions = (userOptions: Partial) => { "showToolbar" in validOptions && validOptions.showToolbar !== undefined; const newOptions = { - ...ReactScanInternals.options.value, + ...getOptionsState(), ...validOptions, }; const { instrumentation } = ReactScanInternals; if (instrumentation && "enabled" in validOptions) { - instrumentation.isPaused.value = validOptions.enabled === false; + instrumentation.setIsPaused(validOptions.enabled === false); } - ReactScanInternals.options.value = newOptions; + setOptionsState(newOptions); // temp hack since defaults override stored local storage values // we actually don't care about any other local storage option other than enabled, we should not be syncing those to local storage @@ -391,7 +352,7 @@ export const setOptions = (userOptions: Partial) => { newOptions.enabled = existing; } } catch (e) { - if (ReactScanInternals.options.value._debug === "verbose") { + if (getOptionsState()._debug === "verbose") { // oxlint-disable-next-line no-console console.error( "[React Scan Internal Error]", @@ -408,12 +369,12 @@ export const setOptions = (userOptions: Partial) => { ); if (shouldInitToolbar) { - initToolbar(!!newOptions.showToolbar); + initToolbar(Boolean(newOptions.showToolbar)); } return newOptions; } catch (e) { - if (ReactScanInternals.options.value._debug === "verbose") { + if (getOptionsState()._debug === "verbose") { // oxlint-disable-next-line no-console console.error( "[React Scan Internal Error]", @@ -462,7 +423,7 @@ export const start = () => { if ( !ReactScanInternals.runInAllEnvironments && getIsProduction() && - !ReactScanInternals.options.value.dangerouslyForceRunInProduction + !getOptionsState().dangerouslyForceRunInProduction ) { return; } @@ -475,17 +436,15 @@ export const start = () => { const validLocalOptions = validateOptions(localStorageOptions); if (Object.keys(validLocalOptions).length > 0) { - ReactScanInternals.options.value = { - ...ReactScanInternals.options.value, + setOptionsState({ + ...getOptionsState(), ...validLocalOptions, - }; + }); } } - const options = getOptions(); - initReactScanInstrumentation(() => { - initToolbar(!!options.value.showToolbar); + initToolbar(Boolean(getOptionsState().showToolbar)); }); if (IS_CLIENT) { @@ -496,7 +455,7 @@ export const start = () => { }, 5000); } } catch (e) { - if (ReactScanInternals.options.value._debug === "verbose") { + if (getOptionsState()._debug === "verbose") { // oxlint-disable-next-line no-console console.error( "[React Scan Internal Error]", @@ -507,54 +466,18 @@ export const start = () => { } }; -const initToolbar = (showToolbar: boolean) => { - window.reactScanCleanupListeners?.(); - - const cleanupTimingTracking = startTimingTracking(); - const cleanupOutlineCanvas = createNotificationsOutlineCanvas(); - - window.reactScanCleanupListeners = () => { - cleanupTimingTracking(); - cleanupOutlineCanvas?.(); - }; - - const windowToolbarContainer = window.__REACT_SCAN_TOOLBAR_CONTAINER__; - - if (!showToolbar) { - windowToolbarContainer?.remove(); - return; - } - - windowToolbarContainer?.remove(); - const { shadowRoot } = initRootContainer(); - createToolbar(shadowRoot); -}; - -const createNotificationsOutlineCanvas = () => { - try { - const highlightRoot = document.documentElement; - return createHighlightCanvas(highlightRoot); - } catch (e) { - if (ReactScanInternals.options.value._debug === "verbose") { - // oxlint-disable-next-line no-console - console.error( - "[React Scan Internal Error]", - "Failed to create notifications outline canvas", - e, - ); - } - } +const initToolbar = (showToolbar: boolean): void => { + createScanRuntime({ + showToolbar, + isVerbose: () => getOptionsState()._debug === "verbose", + }); }; export const scan = (options: Options = {}) => { setOptions(options); - const isInIframe = Store.isInIframe.value; + const isInIframe = getIsInIframe(); - if ( - isInIframe && - !ReactScanInternals.options.value.allowInIframe && - !ReactScanInternals.runInAllEnvironments - ) { + if (isInIframe && !getOptionsState().allowInIframe && !ReactScanInternals.runInAllEnvironments) { return; } @@ -583,9 +506,7 @@ export const onRender = ( }; }; -export const ignoredProps = new WeakSet< - Exclude ->(); +export const ignoredProps = new WeakSet(); export const ignoreScan = (node: ReactNode) => { if (node && typeof node === "object") { diff --git a/packages/scan/src/web/views/inspector/timeline/utils.ts b/packages/scan/src/core/inspection/change-collection.ts similarity index 82% rename from packages/scan/src/web/views/inspector/timeline/utils.ts rename to packages/scan/src/core/inspection/change-collection.ts index 6d47212e..adcde975 100644 --- a/packages/scan/src/web/views/inspector/timeline/utils.ts +++ b/packages/scan/src/core/inspection/change-collection.ts @@ -8,8 +8,8 @@ import { type MemoizedState, SimpleMemoComponentTag, } from "bippy"; -import { isEqual } from "~core/utils"; -import { getChangedPropsDetailed, isPromise } from "../utils"; +import { isEqual } from "../utils"; +import { getChangedPropsDetailed, isPromise } from "./fiber"; interface ChangeTrackingInfo { count: number; @@ -20,18 +20,78 @@ interface ChangeTrackingInfo { type ChangeKey = string | number; +interface TrackedChange { + hasChanged: boolean; + count: number; +} + +interface BaseChange { + name: string | number; + value: unknown; + prevValue: unknown; +} + +interface PropChange extends BaseChange { + name: string; +} + +interface StateChange extends BaseChange { + name: string | number; +} + +interface ContextChange extends BaseChange { + name: string; + contextType: unknown; +} + +interface CollectorResult { + current: Record; + prev: Record; + changes: Array; +} + +interface ContextInfo { + value: unknown; + displayName: string; + contextType: unknown; +} + +export interface SectionData { + current: Array<{ name: string | number; value: unknown }>; + changes: Set; + changesCounts: Map; +} + +export interface InspectorData { + fiberProps: SectionData; + fiberState: SectionData; + fiberContext: SectionData; +} + +export interface InspectorDataResult { + data: InspectorData; + shouldUpdate: boolean; +} + const propsTracker = new Map(); const stateTracker = new Map(); const contextTracker = new Map(); let lastComponentType: unknown = null; const STATE_NAME_REGEX = /\[(?\w+),\s*set\w+\]/g; + +const createEmptySection = (): SectionData => ({ + current: [], + changes: new Set(), + changesCounts: new Map(), +}); + export const getStateNames = (fiber: Fiber): Array => { const componentSource = fiber.type?.toString?.() || ""; return componentSource ? Array.from( componentSource.matchAll(STATE_NAME_REGEX), - (m: RegExpMatchArray) => m.groups?.name ?? "", + (match: RegExpMatchArray) => match.groups?.name ?? "", ) : []; }; @@ -54,13 +114,12 @@ const trackChange = ( key: ChangeKey, currentValue: unknown, previousValue: unknown, -): { hasChanged: boolean; count: number } => { +): TrackedChange => { const existing = tracker.get(key); const isInitialValue = tracker === propsTracker || tracker === contextTracker; const hasChanged = !isEqual(currentValue, previousValue); if (!existing) { - // For props and context, start with count 1 if there's a change tracker.set(key, { count: hasChanged && isInitialValue ? 1 : 0, currentValue, @@ -88,19 +147,7 @@ const trackChange = ( return { hasChanged: false, count: existing.count }; }; -export { propsTracker, stateTracker, contextTracker }; - -export interface SectionData { - current: Array<{ name: string | number; value: unknown }>; - changes: Set; - changesCounts: Map; -} - -export interface InspectorData { - fiberProps: SectionData; - fiberState: SectionData; - fiberContext: SectionData; -} +export { contextTracker, propsTracker, stateTracker }; const getStateFromFiber = (fiber: Fiber): Record => { if (!fiber) return {}; @@ -133,48 +180,16 @@ const getStateFromFiber = (fiber: Fiber): Record => { return {}; }; -export interface InspectorDataResult { - data: InspectorData; - shouldUpdate: boolean; -} - -interface BaseChange { - name: string | number; - value: unknown; - prevValue: unknown; -} - -interface PropChange extends BaseChange { - name: string; -} - -interface StateChange extends BaseChange { - name: string | number; -} - -interface ContextChange extends BaseChange { - name: string; - contextType: unknown; -} - -interface CollectorResult { - current: Record; - prev: Record; - changes: Array; -} - export const collectPropsChanges = (fiber: Fiber): CollectorResult => { const currentProps = fiber.memoizedProps || {}; - const prevProps = fiber.alternate?.memoizedProps || {}; - + const previousProps = fiber.alternate?.memoizedProps || {}; const current: Record = {}; const prev: Record = {}; - const allProps = Object.keys(currentProps); - for (const key of allProps) { + for (const key of Object.keys(currentProps)) { if (key in currentProps) { current[key] = currentProps[key]; - prev[key] = prevProps[key]; + prev[key] = previousProps[key]; } } @@ -208,30 +223,27 @@ export const collectStateChanges = (fiber: Fiber): CollectorResult export const collectContextChanges = (fiber: Fiber): CollectorResult => { const currentContexts = getAllFiberContexts(fiber); - const prevContexts = fiber.alternate ? getAllFiberContexts(fiber.alternate) : new Map(); - + const previousContexts = fiber.alternate ? getAllFiberContexts(fiber.alternate) : new Map(); const current: Record = {}; const prev: Record = {}; const changes: Array = []; - const seenContexts = new Set(); - for (const [contextType, ctx] of currentContexts) { - const name = ctx.displayName; - const contextKey = contextType; - if (seenContexts.has(contextKey)) continue; - seenContexts.add(contextKey); + for (const [contextType, context] of currentContexts) { + const name = context.displayName; + if (seenContexts.has(contextType)) continue; + seenContexts.add(contextType); - current[name] = ctx.value; + current[name] = context.value; - const prevCtx = prevContexts.get(contextType); - if (prevCtx) { - prev[name] = prevCtx.value; - if (!isEqual(prevCtx.value, ctx.value)) { + const previousContext = previousContexts.get(contextType); + if (previousContext) { + prev[name] = previousContext.value; + if (!isEqual(previousContext.value, context.value)) { changes.push({ name, - value: ctx.value, - prevValue: prevCtx.value, + value: context.value, + prevValue: previousContext.value, contextType, }); } @@ -242,18 +254,12 @@ export const collectContextChanges = (fiber: Fiber): CollectorResult { - const emptySection = (): SectionData => ({ - current: [], - changes: new Set(), - changesCounts: new Map(), - }); - if (!fiber) { return { data: { - fiberProps: emptySection(), - fiberState: emptySection(), - fiberContext: emptySection(), + fiberProps: createEmptySection(), + fiberState: createEmptySection(), + fiberContext: createEmptySection(), }, shouldUpdate: false, }; @@ -261,8 +267,8 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { let hasNewChanges = false; const isInitialUpdate = isInitialComponentUpdate(fiber); + const propsData = createEmptySection(); - const propsData = emptySection(); if (fiber.memoizedProps) { const { current, changes } = collectPropsChanges(fiber); @@ -289,7 +295,7 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { } } - const stateData = emptySection(); + const stateData = createEmptySection(); const { current: stateCurrent, changes: stateChanges } = collectStateChanges(fiber); for (const [index, value] of Object.entries(stateCurrent)) { @@ -312,7 +318,7 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { } } - const contextData = emptySection(); + const contextData = createEmptySection(); const { current: contextCurrent, changes: contextChanges } = collectContextChanges(fiber); for (const [name, value] of Object.entries(contextCurrent)) { @@ -352,15 +358,10 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { }; }; -interface ContextInfo { - value: unknown; - displayName: string; - contextType: unknown; -} // hm we potentially want to revalidate this if a fiber has new context's, i'm not sure how we can do that reactively // i suppose we can do one traversal on render (or during the existing traversal) that checks if any new context providers were mounted // and when that happens we revalidate this cache - +// // i suppose a case this breaks is if a fiber changes ancestors through a key but doesn't remount // then it would have new parents... and that new parent may have new context // may be a fine trade off @@ -372,8 +373,6 @@ export const getAllFiberContexts = (fiber: Fiber): Map => return new Map(); } - // todo validate this works - const cachedContexts = fiberContextsCache.get(fiber); if (cachedContexts) { return cachedContexts; @@ -411,30 +410,20 @@ export const getAllFiberContexts = (fiber: Fiber): Map => currentFiber = currentFiber.return; } - // Cache the result for this fiber fiberContextsCache.set(fiber, contexts); - return contexts; }; -export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { - const emptySection = (): SectionData => ({ - current: [], - changes: new Set(), - changesCounts: new Map(), - }); - +export const collectInspectorDataWithoutCounts = (fiber: Fiber): InspectorData => { if (!fiber) { return { - fiberProps: emptySection(), - fiberState: emptySection(), - fiberContext: emptySection(), + fiberProps: createEmptySection(), + fiberState: createEmptySection(), + fiberContext: createEmptySection(), }; } - // let hasNewChanges = false; - - const propsData = emptySection(); + const propsData = createEmptySection(); if (fiber.memoizedProps) { const { current, changes } = collectPropsChanges(fiber); @@ -446,13 +435,12 @@ export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { } for (const change of changes) { - // hasNewChanges = true; propsData.changes.add(change.name); propsData.changesCounts.set(change.name, 1); } } - const stateData = emptySection(); + const stateData = createEmptySection(); if (fiber.memoizedState) { const { current, changes } = collectStateChanges(fiber); @@ -464,13 +452,12 @@ export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { } for (const change of changes) { - // hasNewChanges = true; stateData.changes.add(change.name); stateData.changesCounts.set(change.name, 1); } } - const contextData = emptySection(); + const contextData = createEmptySection(); const { current, changes } = collectContextChanges(fiber); for (const [key, value] of Object.entries(current)) { @@ -481,22 +468,13 @@ export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { } for (const change of changes) { - // hasNewChanges = true; contextData.changes.add(change.name); contextData.changesCounts.set(change.name, 1); } - // todo: is isInitialUpdate correct? Is this necessary: - // if (!hasNewChanges && !isInitialUpdate) { - // propsData.changes.clear(); - // stateData.changes.clear(); - // contextData.changes.clear(); - // } return { - // data: { fiberProps: propsData, fiberState: stateData, fiberContext: contextData, - // }, }; }; diff --git a/packages/scan/src/core/inspection/fiber.ts b/packages/scan/src/core/inspection/fiber.ts new file mode 100644 index 00000000..ff9008eb --- /dev/null +++ b/packages/scan/src/core/inspection/fiber.ts @@ -0,0 +1,94 @@ +import { type Fiber, isCompositeFiber, isHostFiber } from "bippy"; +import { ChangeReason } from "../instrumentation"; +import { isEqual } from "../utils"; + +interface ReactRootContainer { + _reactRootContainer?: { + _internalRoot?: { + current?: { + child: Fiber; + }; + }; + }; +} + +interface ReactInternalProps { + [key: string]: Fiber; +} + +export interface DetailedPropsChange { + name: string; + value: unknown; + prevValue: unknown; + type: ChangeReason.Props; +} + +export const getFiberFromElement = (element: Element): Fiber | null => { + if ("__REACT_DEVTOOLS_GLOBAL_HOOK__" in window) { + const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; + if (!hook?.renderers) return null; + + for (const [, renderer] of Array.from(hook.renderers)) { + try { + const fiber = renderer.findFiberByHostInstance?.(element); + if (fiber) return fiber; + } catch { + // If React is mid-render, references to previous nodes may disappear + } + } + } + + if ("_reactRootContainer" in element) { + const elementWithRoot = element as unknown as ReactRootContainer; + return elementWithRoot._reactRootContainer?._internalRoot?.current?.child ?? null; + } + + for (const key in element) { + if (key.startsWith("__reactInternalInstance$") || key.startsWith("__reactFiber")) { + const elementWithFiber = element as unknown as ReactInternalProps; + return elementWithFiber[key]; + } + } + + return null; +}; + +export const getParentCompositeFiber = (fiber: Fiber): readonly [Fiber, Fiber | null] | null => { + let current: Fiber | null = fiber; + let previousHost: Fiber | null = null; + + while (current) { + if (isCompositeFiber(current)) return [current, previousHost] as const; + if (isHostFiber(current) && !previousHost) previousHost = current; + current = current.return; + } + + return null; +}; + +export const getChangedPropsDetailed = (fiber: Fiber): Array => { + const currentProps = fiber.memoizedProps ?? {}; + const previousProps = fiber.alternate?.memoizedProps ?? {}; + const changes: Array = []; + + for (const key in currentProps) { + if (key === "children") continue; + + const currentValue = currentProps[key]; + const previousValue = previousProps[key]; + + if (!isEqual(currentValue, previousValue)) { + changes.push({ + name: key, + value: currentValue, + prevValue: previousValue, + type: ChangeReason.Props, + }); + } + } + + return changes; +}; + +export const isPromise = (value: unknown): value is Promise => + value instanceof Promise || (typeof value === "object" && value !== null && "then" in value); diff --git a/packages/scan/src/core/inspection/types.ts b/packages/scan/src/core/inspection/types.ts new file mode 100644 index 00000000..70d54269 --- /dev/null +++ b/packages/scan/src/core/inspection/types.ts @@ -0,0 +1,22 @@ +import type { Fiber } from "bippy"; + +export interface InspectingState { + kind: "inspecting"; + hoveredDomElement: Element | null; +} + +export interface InspectOffState { + kind: "inspect-off"; +} + +export interface FocusedState { + kind: "focused"; + focusedDomElement: Element; + fiber: Fiber; +} + +export interface UninitializedState { + kind: "uninitialized"; +} + +export type States = InspectingState | InspectOffState | FocusedState | UninitializedState; diff --git a/packages/scan/src/core/instrumentation.ts b/packages/scan/src/core/instrumentation.ts index 5fb75788..fc5864f8 100644 --- a/packages/scan/src/core/instrumentation.ts +++ b/packages/scan/src/core/instrumentation.ts @@ -1,4 +1,5 @@ -import { type Signal, signal } from "@preact/signals"; +import { type Accessor } from "solid-js"; +import { createCompatibleSignal, type Signal } from "./compatibility"; import { ClassComponentTag, type Fiber, @@ -20,14 +21,15 @@ import { traverseProps, traverseRenderedFibers, } from "bippy"; -import { isValidElement } from "preact"; -import { isEqual } from "~core/utils"; +import { isValidElement } from "react"; +import { isEqual } from "./utils"; import { collectContextChanges, collectPropsChanges, collectStateChanges, -} from "~web/views/inspector/timeline/utils"; -import { type Change, type ContextChange, ReactScanInternals, type StateChange } from "./index"; +} from "./inspection/change-collection"; +import type { Change, ContextChange, StateChange } from "./index"; +import { getOptionsState } from "./native-state"; export enum RenderPhase { Mount = 0b001, @@ -87,16 +89,6 @@ export const getFPS = () => { return fps; }; -const isElementVisible = (el: Element) => { - const style = window.getComputedStyle(el); - return ( - style.display !== "none" && - style.visibility !== "hidden" && - style.contentVisibility !== "hidden" && - style.opacity !== "0" - ); -}; - export const isValueUnstable = (prevValue: unknown, nextValue: unknown) => { const prevValueString = fastSerialize(prevValue); const nextValueString = fastSerialize(nextValue); @@ -107,16 +99,6 @@ export const isValueUnstable = (prevValue: unknown, nextValue: unknown) => { ); }; -const isElementInViewport = (el: Element, rect = el.getBoundingClientRect()) => { - const isVisible = - rect.bottom > 0 && - rect.right > 0 && - rect.top < window.innerHeight && - rect.left < window.innerWidth; - - return isVisible && rect.width && rect.height; -}; - export const enum ChangeReason { Props = 0b001, FunctionalState = 0b010, @@ -344,6 +326,8 @@ interface InstrumentationInstance { interface Instrumentation { isPaused: Signal; + getIsPaused: Accessor; + setIsPaused: (isPaused: boolean) => boolean; fiberRoots: WeakSet; } @@ -385,7 +369,7 @@ const isRenderUnnecessary = (fiber: Fiber) => { // // re-implement this in new-outlines // const shouldRunUnnecessaryRenderCheck = () => { // // yes, this can be condensed into one conditional, but ifs are easier to reason/build on than long boolean expressions -// if (!ReactScanInternals.options.value.trackUnnecessaryRenders) { +// if (!getOptionsState().trackUnnecessaryRenders) { // return false; // } @@ -393,8 +377,8 @@ const isRenderUnnecessary = (fiber: Fiber) => { // if ( // getIsProduction() && // Store.monitor.value && -// ReactScanInternals.options.value.dangerouslyForceRunInProduction && -// ReactScanInternals.options.value.trackUnnecessaryRenders +// getOptionsState().dangerouslyForceRunInProduction && +// getOptionsState().trackUnnecessaryRenders // ) { // return true; // } @@ -403,7 +387,7 @@ const isRenderUnnecessary = (fiber: Fiber) => { // return false; // } -// return ReactScanInternals.options.value.trackUnnecessaryRenders; +// return getOptionsState().trackUnnecessaryRenders; // }; const TRACK_UNNECESSARY_RENDERS = false; @@ -490,9 +474,12 @@ const trackRender = ( }; export const createInstrumentation = (instanceKey: string, config: InstrumentationConfig) => { + const pausedState = createCompatibleSignal(!getOptionsState().enabled); const instrumentation: Instrumentation = { // this will typically be false, but in cases where a user provides showToolbar: true, this will be true - isPaused: signal(!ReactScanInternals.options.value.enabled), + isPaused: pausedState.facade, + getIsPaused: pausedState.get, + setIsPaused: pausedState.set, fiberRoots: new WeakSet(), }; instrumentationInstances.set(instanceKey, { @@ -510,9 +497,9 @@ export const createInstrumentation = (instanceKey: string, config: Instrumentati instrumentation.fiberRoots.add(root); // for now we always track everything for notifications, it may be worth it to make this configurable // if ( - // ReactScanInternals.instrumentation?.isPaused.value && - // (Store.inspectState.value.kind === "inspect-off" || - // Store.inspectState.value.kind === "uninitialized") && + // instrumentation.getIsPaused() && + // (getInspectState().kind === "inspect-off" || + // getInspectState().kind === "uninitialized") && // !config.forceAlwaysTrackRenders // ) { // return; diff --git a/packages/scan/src/core/native-state.ts b/packages/scan/src/core/native-state.ts new file mode 100644 index 00000000..a9e4c117 --- /dev/null +++ b/packages/scan/src/core/native-state.ts @@ -0,0 +1,42 @@ +import type { Fiber } from "bippy"; +import { createCompatibleSignal } from "./compatibility"; +import type { Options, StoreType } from "./index"; +import type { States } from "./inspection/types"; +import type { RenderData } from "./utils"; +import { IS_CLIENT } from "../utils/is-client"; + +const wasDetailsOpenState = createCompatibleSignal(true); +const isInIframeState = createCompatibleSignal(IS_CLIENT && window.self !== window.top); +const inspectState = createCompatibleSignal({ kind: "uninitialized" }); +const lastReportTimeState = createCompatibleSignal(0); +const optionsState = createCompatibleSignal({ + enabled: true, + log: false, + showToolbar: true, + animationSpeed: "fast", + dangerouslyForceRunInProduction: false, + showFPS: true, + showNotificationCount: true, + allowInIframe: false, +}); + +export const getInspectState = inspectState.get; +export const setInspectState = inspectState.set; +export const getLastReportTime = lastReportTimeState.get; +export const setLastReportTime = lastReportTimeState.set; +export const getIsInIframe = isInIframeState.get; +export const getOptionsState = optionsState.get; +export const setOptionsState = optionsState.set; +export const optionsFacade = optionsState.facade; + +export const Store: StoreType = { + wasDetailsOpen: wasDetailsOpenState.facade, + isInIframe: isInIframeState.facade, + inspectState: inspectState.facade, + fiberRoots: new Set(), + reportData: new Map(), + legacyReportData: new Map(), + lastReportTime: lastReportTimeState.facade, + interactionListeningForRenders: null, + changesListeners: new Map(), +}; diff --git a/packages/scan/src/core/notifications/event-tracking.ts b/packages/scan/src/core/notifications/event-tracking.ts index d0a05000..f7dcc19f 100644 --- a/packages/scan/src/core/notifications/event-tracking.ts +++ b/packages/scan/src/core/notifications/event-tracking.ts @@ -1,18 +1,16 @@ -import { useSyncExternalStore } from "preact/compat"; -import { not_globally_unique_generateId } from "~core/utils"; -import { MAX_INTERACTION_BATCH, interactionStore } from "./interaction-store"; +import { not_globally_unique_generateId } from "../utils"; import { FiberRenders, PerformanceEntryChannelEvent, TimeoutStage, + clearPerformanceEntries, + hasPendingPerformanceEntries, listenForPerformanceEntryInteractions, listenForRenders, setupDetailedPointerTimingListener, setupPerformancePublisher, } from "./performance"; -import { MAX_CHANNEL_SIZE, performanceEntryChannels } from "./performance-store"; import { BoundedArray } from "./performance-utils"; -import { createStore } from "~web/utils/create-store"; type FinalInteraction = { detailedTiming: TimeoutStage; @@ -51,130 +49,102 @@ export type SlowdownEvent = (InteractionEvent | LongRenderPipeline) & { id: string; }; -type ToolbarEventStoreState = { - state: { - events: BoundedArray; - }; - actions: { - addEvent: (event: SlowdownEvent) => void; - addListener: (listener: (event: SlowdownEvent) => void) => () => void; - clear: () => void; +const EVENT_STORE_CAPACITY = 200; +let toolbarEvents = new BoundedArray(EVENT_STORE_CAPACITY); +const toolbarEventSubscribers = new Set<() => void>(); + +export const getToolbarEvents = (): BoundedArray => toolbarEvents; + +export const subscribeToolbarEvents = (subscriber: () => void): (() => void) => { + toolbarEventSubscribers.add(subscriber); + return () => { + toolbarEventSubscribers.delete(subscriber); }; }; -const EVENT_STORE_CAPACITY = 200; +export const clearToolbarEvents = (): void => { + toolbarEvents = new BoundedArray(EVENT_STORE_CAPACITY); + toolbarEventSubscribers.forEach((subscriber) => subscriber()); +}; -export const toolbarEventStore = createStore()((set, get) => { - const listeners = new Set<(event: SlowdownEvent) => void>(); - - return { - state: { - events: new BoundedArray(EVENT_STORE_CAPACITY), - }, - - actions: { - addEvent: (event: SlowdownEvent) => { - listeners.forEach((listener) => listener(event)); - - const events = [...get().state.events, event]; - const applyOverlapCheckToLongRenderEvent = ( - longRenderEvent: LongRenderPipeline & { id: string }, - onOverlap: (overlapsWith: InteractionEvent & { id: string }) => void, - ) => { - const overlapsWith = events.find((event) => { - if (event.kind === "long-render") { - return; - } - - if (event.id === longRenderEvent.id) { - return; - } - - /** - * |---x-----------x------ (interaction) - * |x-----------x (long-render) - */ +const addToolbarEvent = (event: SlowdownEvent): void => { + const events = [...toolbarEvents, event]; + const applyOverlapCheckToLongRenderEvent = ( + longRenderEvent: LongRenderPipeline & { id: string }, + onOverlap: (overlapsWith: InteractionEvent & { id: string }) => void, + ) => { + const overlapsWith = events.find( + (candidateEvent): candidateEvent is InteractionEvent & { id: string } => { + if (candidateEvent.kind === "long-render") { + return false; + } - if ( - longRenderEvent.data.startAt <= event.data.startAt && - longRenderEvent.data.endAt <= event.data.endAt && - longRenderEvent.data.endAt >= event.data.startAt - ) { - return true; - } + if (candidateEvent.id === longRenderEvent.id) { + return false; + } + + /** + * |---x-----------x------ (interaction) + * |x-----------x (long-render) + */ + + if ( + longRenderEvent.data.startAt <= candidateEvent.data.startAt && + longRenderEvent.data.endAt <= candidateEvent.data.endAt && + longRenderEvent.data.endAt >= candidateEvent.data.startAt + ) { + return true; + } - /** + /** * |x-----------x---- (interaction) * |--x------------x (long-render) * */ - if ( - event.data.startAt <= longRenderEvent.data.startAt && - event.data.endAt >= longRenderEvent.data.startAt - ) { - return true; - } - - /** - * - * |--x-------------x (interaction) - * |x------------------x (long-render) - * - */ - - if ( - longRenderEvent.data.startAt <= event.data.startAt && - longRenderEvent.data.endAt >= event.data.endAt - ) { - return true; - } - }) as undefined | (InteractionEvent & { id: string }); // invariant: because we early check the typechecker does not know it must be the case that when it finds something, it will be an interaction it overlaps with - - if (overlapsWith) { - onOverlap(overlapsWith); - } - }; - - const toRemove = new Set(); - - events.forEach((event) => { - if (event.kind === "interaction") return; - applyOverlapCheckToLongRenderEvent(event, () => { - toRemove.add(event.id); - }); - }); - - const withRemovedEvents = events.filter((event) => !toRemove.has(event.id)); + if ( + candidateEvent.data.startAt <= longRenderEvent.data.startAt && + candidateEvent.data.endAt >= longRenderEvent.data.startAt + ) { + return true; + } - set(() => ({ - state: { - events: BoundedArray.fromArray(withRemovedEvents, EVENT_STORE_CAPACITY), - }, - })); - }, + /** + * + * |--x-------------x (interaction) + * |x------------------x (long-render) + * + */ - addListener: (listener: (event: SlowdownEvent) => void) => { - listeners.add(listener); - return () => { - listeners.delete(listener); - }; + if ( + longRenderEvent.data.startAt <= candidateEvent.data.startAt && + longRenderEvent.data.endAt >= candidateEvent.data.endAt + ) { + return true; + } + return false; }, + ); - clear: () => { - set({ - state: { - events: new BoundedArray(EVENT_STORE_CAPACITY), - }, - }); - }, - }, + if (overlapsWith) { + onOverlap(overlapsWith); + } }; -}); -export const useToolbarEventLog = () => { - return useSyncExternalStore(toolbarEventStore.subscribe, toolbarEventStore.getState); + const eventIdsToRemove = new Set(); + events.forEach((candidateEvent) => { + if (candidateEvent.kind === "interaction") return; + applyOverlapCheckToLongRenderEvent(candidateEvent, () => { + eventIdsToRemove.add(candidateEvent.id); + }); + }); + + toolbarEvents = BoundedArray.fromArray( + events.filter((candidateEvent) => !eventIdsToRemove.has(candidateEvent.id)), + EVENT_STORE_CAPACITY, + ); + toolbarEventSubscribers.forEach((subscriber) => subscriber()); }; let taskDirtyAt: null | number = null; @@ -263,7 +233,7 @@ function startLongPipelineTracking() { const endAt = endOrigin + endNow; const startAt = startTime + startOrigin; - toolbarEventStore.getState().actions.addEvent({ + addToolbarEvent({ kind: "long-render", id: not_globally_unique_generateId(), data: { @@ -308,7 +278,7 @@ export const startTimingTracking = () => { finalInteraction: FinalInteraction, event: PerformanceEntryChannelEvent, ) => { - toolbarEventStore.getState().actions.addEvent({ + addToolbarEvent({ kind: "interaction", id: not_globally_unique_generateId(), data: { @@ -318,18 +288,13 @@ export const startTimingTracking = () => { }, }); - const existingCompletedInteractions = performanceEntryChannels.getChannelState("recording"); - finalInteraction.detailedTiming.stopListeningForRenders(); - if (existingCompletedInteractions.length) { + if (hasPendingPerformanceEntries()) { // then performance entry and our detailed timing handlers are out of sync, we disregard that entry // it may be possible the performance entry returned before detailed timing. If that's the case we should update // assumptions and deal with mapping the entry back to the detailed timing here - performanceEntryChannels.updateChannelState( - "recording", - () => new BoundedArray(MAX_CHANNEL_SIZE), - ); + clearPerformanceEntries(); } }; const unSubDetailedPointerTiming = setupDetailedPointerTimingListener("pointer", { @@ -339,14 +304,7 @@ export const startTimingTracking = () => { onComplete, }); - const unSubInteractions = listenForPerformanceEntryInteractions((completedInteraction) => { - interactionStore.setState( - BoundedArray.fromArray( - interactionStore.getCurrentState().concat(completedInteraction), - MAX_INTERACTION_BATCH, - ), - ); - }); + const unSubInteractions = listenForPerformanceEntryInteractions(); return () => { unSubMouseOver(); diff --git a/packages/scan/src/core/notifications/interaction-store.ts b/packages/scan/src/core/notifications/interaction-store.ts deleted file mode 100644 index 1ef4ab26..00000000 --- a/packages/scan/src/core/notifications/interaction-store.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BoundedArray } from "~core/notifications/performance-utils"; -import { CompletedInteraction } from "./performance"; - -type Subscriber = (data: T) => void; - -class Store { - private subscribers: Set> = new Set(); - private currentValue: T; - - constructor(initialValue: T) { - this.currentValue = initialValue; - } - - subscribe(subscriber: Subscriber): () => void { - this.subscribers.add(subscriber); - - subscriber(this.currentValue); - - return () => { - this.subscribers.delete(subscriber); - }; - } - - setState(data: T) { - this.currentValue = data; - this.subscribers.forEach((subscriber) => subscriber(data)); - } - - getCurrentState(): T { - return this.currentValue; - } -} -export const MAX_INTERACTION_BATCH = 150; -export const interactionStore = new Store>( - new BoundedArray(MAX_INTERACTION_BATCH), -); diff --git a/packages/scan/src/core/notifications/outline-overlay.ts b/packages/scan/src/core/notifications/outline-overlay.ts index 2eafa099..42d98cea 100644 --- a/packages/scan/src/core/notifications/outline-overlay.ts +++ b/packages/scan/src/core/notifications/outline-overlay.ts @@ -1,4 +1,4 @@ -import { signal } from "@preact/signals"; +import { createSignal } from "solid-js"; import { iife } from "./performance-utils"; let highlightCanvas: HTMLCanvasElement | null = null; @@ -37,11 +37,20 @@ type HighlightState = } | null; }; -export const HighlightStore = signal({ +const [getHighlightState, setHighlightStateValue] = /* @__PURE__ */ createSignal({ kind: "idle", current: null, }); +export { getHighlightState }; + +export const setHighlightState = (state: HighlightState) => { + setHighlightStateValue(state); + requestAnimationFrame(() => { + drawHighlights(); + }); +}; + let currFrame: ReturnType | null = null; let lastFrameTime = 0; const FADE_SPEED = 1.8; @@ -66,7 +75,7 @@ export const drawHighlights = () => { highlightCtx.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height); const color = "hsl(271, 76%, 53%)"; - const state = HighlightStore.value; + const state = getHighlightState(); const { alpha, current } = iife(() => { switch (state.kind) { case "transition": { @@ -113,10 +122,10 @@ export const drawHighlights = () => { switch (state.kind) { case "move-out": { if (state.current.alpha === 0) { - HighlightStore.value = { + setHighlightState({ kind: "idle", current: null, - }; + }); lastFrameTime = 0; return; } @@ -136,10 +145,10 @@ export const drawHighlights = () => { // invariant, state.current.alpha === 0 if (state.transitionTo.alpha === 1) { - HighlightStore.value = { + setHighlightState({ kind: "idle", current: state.transitionTo, - }; + }); lastFrameTime = 0; return; } @@ -201,12 +210,6 @@ export const createHighlightCanvas = (root: HTMLElement) => { window.addEventListener("resize", handleResize); - HighlightStore.subscribe(() => { - requestAnimationFrame(() => { - drawHighlights(); - }); - }); - return cleanup; }; diff --git a/packages/scan/src/core/notifications/performance-store.ts b/packages/scan/src/core/notifications/performance-store.ts deleted file mode 100644 index 36af15b8..00000000 --- a/packages/scan/src/core/notifications/performance-store.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { BoundedArray } from "./performance-utils"; -import { PerformanceEntryChannelEvent } from "./performance"; - -type UnSubscribe = () => void; -type Callback = (item: T) => void; -type Updater = (state: BoundedArray) => BoundedArray; -type ChanelName = string; - -type PerformanceEntryChannelsType = { - subscribe: (to: ChanelName, cb: Callback) => UnSubscribe; - publish: ( - item: T, - to: ChanelName, - dropFirst: boolean, - createIfNoChannel: boolean - ) => void; - channels: Record< - ChanelName, - { callbacks: BoundedArray>; state: BoundedArray } - >; - getAvailableChannels: () => BoundedArray; - updateChannelState: ( - channel: ChanelName, - updater: Updater, - createIfNoChannel: boolean - ) => void; -}; - -export const MAX_CHANNEL_SIZE = 50; -// a set of entities communicate to each other through channels -// the state in the channel is persisted until the receiving end consumes it -// multiple subscribes to the same channel will likely lead to unintended behavior if the subscribers are separate entities -class PerformanceEntryChannels implements PerformanceEntryChannelsType { - channels: PerformanceEntryChannelsType["channels"] = {}; - publish(item: T, to: ChanelName, createIfNoChannel = true) { - const existingChannel = this.channels[to]; - if (!existingChannel) { - if (!createIfNoChannel) { - return; - } - this.channels[to] = { - callbacks: new BoundedArray>(MAX_CHANNEL_SIZE), - state: new BoundedArray(MAX_CHANNEL_SIZE), - }; - this.channels[to].state.push(item); - return; - } - - existingChannel.state.push(item); - existingChannel.callbacks.forEach((cb) => cb(item)); - } - - getAvailableChannels() { - return BoundedArray.fromArray(Object.keys(this.channels), MAX_CHANNEL_SIZE); - } - subscribe(to: ChanelName, cb: Callback, dropFirst: boolean = false) { - const defer = () => { - if (!dropFirst) { - this.channels[to].state.forEach((item) => { - cb(item); - }); - } - return () => { - const filtered = this.channels[to].callbacks.filter( - (subscribed) => subscribed !== cb - ); - this.channels[to].callbacks = BoundedArray.fromArray( - filtered, - MAX_CHANNEL_SIZE - ); - }; - }; - const existing = this.channels[to]; - if (!existing) { - this.channels[to] = { - callbacks: new BoundedArray>(MAX_CHANNEL_SIZE), - state: new BoundedArray(MAX_CHANNEL_SIZE), - }; - this.channels[to].callbacks.push(cb); - return defer(); - } - - existing.callbacks.push(cb); - return defer(); - } - updateChannelState( - channel: ChanelName, - updater: Updater, - createIfNoChannel = true - ) { - const existingChannel = this.channels[channel]; - if (!existingChannel) { - if (!createIfNoChannel) { - return; - } - - const state = new BoundedArray(MAX_CHANNEL_SIZE); - const newChannel = { - callbacks: new BoundedArray>(MAX_CHANNEL_SIZE), - state, - }; - - this.channels[channel] = newChannel; - newChannel.state = updater(state); - return; - } - - existingChannel.state = updater(existingChannel.state); - } - - getChannelState(channel: ChanelName) { - return ( - this.channels[channel].state ?? new BoundedArray(MAX_CHANNEL_SIZE) - ); - } -} -// todo: discriminated union the events when we start using multiple channels -// we used to use multiple channels, but now we only use 1. This is still a useful abstraction incase we ever need more channels again -export const performanceEntryChannels = - new PerformanceEntryChannels(); diff --git a/packages/scan/src/core/notifications/performance.ts b/packages/scan/src/core/notifications/performance.ts index 527142e8..75d7d7b9 100644 --- a/packages/scan/src/core/notifications/performance.ts +++ b/packages/scan/src/core/notifications/performance.ts @@ -1,15 +1,11 @@ import { Fiber, getDisplayName, getTimings, hasMemoCache, isHostFiber, traverseFiber } from "bippy"; -import { Store } from "../.."; - -import { BoundedArray, invariantError } from "~core/notifications/performance-utils"; -import { - SectionData, - collectInspectorDataWithoutCounts, -} from "~web/views/inspector/timeline/utils"; -import { getFiberFromElement, getParentCompositeFiber } from "~web/views/inspector/utils"; -import { performanceEntryChannels } from "./performance-store"; +import { Store } from "../native-state"; + +import { BoundedArray, invariantError } from "./performance-utils"; +import { SectionData, collectInspectorDataWithoutCounts } from "../inspection/change-collection"; +import { getFiberFromElement, getParentCompositeFiber } from "../inspection/fiber"; import type { PerformanceInteraction, PerformanceInteractionEntry } from "./types"; -import { not_globally_unique_generateId } from "~core/utils"; +import { not_globally_unique_generateId } from "../utils"; interface PathFilters { skipProviders: boolean; @@ -219,6 +215,31 @@ export type PerformanceEntryChannelEvent = detailedTiming: TimeoutStage; }; +const MAX_PERFORMANCE_ENTRIES = 50; +let performanceEntries = new BoundedArray(MAX_PERFORMANCE_ENTRIES); +const performanceEntrySubscribers = new Set<(event: PerformanceEntryChannelEvent) => void>(); + +const publishPerformanceEntry = (event: PerformanceEntryChannelEvent): void => { + performanceEntries.push(event); + performanceEntrySubscribers.forEach((subscriber) => subscriber(event)); +}; + +const subscribePerformanceEntries = ( + subscriber: (event: PerformanceEntryChannelEvent) => void, +): (() => void) => { + performanceEntries.forEach((event) => subscriber(event)); + performanceEntrySubscribers.add(subscriber); + return () => { + performanceEntrySubscribers.delete(subscriber); + }; +}; + +export const hasPendingPerformanceEntries = (): boolean => performanceEntries.length > 0; + +export const clearPerformanceEntries = (): void => { + performanceEntries = new BoundedArray(MAX_PERFORMANCE_ENTRIES); +}; + export type CompletedInteraction = { detailedTiming: TimeoutStage; latency: number; @@ -360,13 +381,10 @@ const setupPerformanceListener = (onEntry: (interaction: PerformanceInteraction) export const setupPerformancePublisher = () => { return setupPerformanceListener((entry) => { - performanceEntryChannels.publish( - { - kind: "entry-received", - entry, - }, - "recording", - ); + publishPerformanceEntry({ + kind: "entry-received", + entry, + }); }); }; @@ -409,11 +427,9 @@ const getAssociatedDetailedTimingInteraction = ( }; // this would be cool if it listened for merge, so it had to be after -export const listenForPerformanceEntryInteractions = ( - onComplete: (completedInteraction: CompletedInteraction) => void, -) => { +export const listenForPerformanceEntryInteractions = () => { // we make the assumption that the detailed timing will be ready before the performance timing - const unsubscribe = performanceEntryChannels.subscribe("recording", (event) => { + const unsubscribe = subscribePerformanceEntries((event) => { const associatedDetailedInteraction = event.kind === "auto-complete-race" ? tasks.find((task) => task.interactionUUID === event.interactionUUID) @@ -426,8 +442,7 @@ export const listenForPerformanceEntryInteractions = ( return; } - const completedInteraction = associatedDetailedInteraction.completeInteraction(event); - onComplete(completedInteraction); + associatedDetailedInteraction.completeInteraction(event); }); return unsubscribe; diff --git a/packages/scan/src/core/utils.ts b/packages/scan/src/core/utils.ts index d4ac2e2b..479ea1e2 100644 --- a/packages/scan/src/core/utils.ts +++ b/packages/scan/src/core/utils.ts @@ -1,6 +1,5 @@ -// @ts-nocheck import type { AggregatedRender, Render } from "./instrumentation"; -import { IS_CLIENT } from "~web/utils/constants"; +import { IS_CLIENT } from "../utils/is-client"; function descending(a: number, b: number): number { return b - a; @@ -113,7 +112,7 @@ export interface RenderData { renders: Array; displayName: string | null; type: unknown; - changes?: Array; + changes?: Render["changes"]; } export function isEqual(a: unknown, b: unknown): boolean { @@ -125,12 +124,9 @@ export const not_globally_unique_generateId = () => { return "0"; } - // @ts-expect-error if (window.reactScanIdCounter === undefined) { - // @ts-expect-error window.reactScanIdCounter = 0; } - // @ts-expect-error return `${++window.reactScanIdCounter}`; }; diff --git a/packages/scan/src/index.ts b/packages/scan/src/index.ts index 02245d2e..c7ca47a3 100644 --- a/packages/scan/src/index.ts +++ b/packages/scan/src/index.ts @@ -1,5 +1,5 @@ -import './polyfills'; +import "./polyfills"; // Bippy has a side-effect that installs the hook. -import 'bippy'; +import "bippy"; -export * from './core/index'; +export * from "./core/index"; diff --git a/packages/scan/src/install-hook.ts b/packages/scan/src/install-hook.ts index 0a29ca86..c956ffe4 100644 --- a/packages/scan/src/install-hook.ts +++ b/packages/scan/src/install-hook.ts @@ -1 +1 @@ -export { getRDTHook as init } from 'bippy'; +export { getRDTHook as init } from "bippy"; diff --git a/packages/scan/src/lite/change-description.ts b/packages/scan/src/lite/change-description.ts index 0e8d3b97..4815dd65 100644 --- a/packages/scan/src/lite/change-description.ts +++ b/packages/scan/src/lite/change-description.ts @@ -8,8 +8,8 @@ import { traverseContexts, traverseProps, traverseState, -} from 'bippy'; -import type { ChangeDescription } from './types'; +} from "bippy"; +import type { ChangeDescription } from "./types"; const objectIs = Object.is; @@ -59,17 +59,14 @@ const didAnyClassStateChange = (fiber: Fiber): boolean => { if ( !previousState || !nextState || - typeof previousState !== 'object' || - typeof nextState !== 'object' + typeof previousState !== "object" || + typeof nextState !== "object" ) { return previousState !== nextState; } const previousObject = previousState as Record; const nextObject = nextState as Record; - const allKeys = new Set([ - ...Object.keys(previousObject), - ...Object.keys(nextObject), - ]); + const allKeys = new Set([...Object.keys(previousObject), ...Object.keys(nextObject)]); for (const key of allKeys) { if (!objectIs(previousObject[key], nextObject[key])) return true; } @@ -91,11 +88,7 @@ const collectChangedHookIndices = (fiber: Fiber): Array => { const indices: Array = []; let index = 0; traverseState(fiber, (nextState, prevState) => { - if ( - nextState && - prevState && - !objectIs(prevState.memoizedState, nextState.memoizedState) - ) { + if (nextState && prevState && !objectIs(prevState.memoizedState, nextState.memoizedState)) { indices.push(index); } index++; diff --git a/packages/scan/src/lite/constants.ts b/packages/scan/src/lite/constants.ts index 5b838e01..2373c4e2 100644 --- a/packages/scan/src/lite/constants.ts +++ b/packages/scan/src/lite/constants.ts @@ -1,4 +1,4 @@ -export const DEFAULT_LOCATION = 'ReactScanLite'; +export const DEFAULT_LOCATION = "ReactScanLite"; export const DEFAULT_MAX_FIBERS_PER_COMMIT = 5000; export const DEFAULT_MIN_FIBER_ACTUAL_DURATION_MS = 0; @@ -9,10 +9,10 @@ export const REACT_TOTAL_NUM_LANES = 31; // Scheduler priority levels. Source: // https://github.com/facebook/react/blob/main/packages/scheduler/src/SchedulerPriorities.js export const SCHEDULER_PRIORITY_NAMES: Record = { - 0: 'NoPriority', - 1: 'Immediate', - 2: 'UserBlocking', - 3: 'Normal', - 4: 'Low', - 5: 'Idle', + 0: "NoPriority", + 1: "Immediate", + 2: "UserBlocking", + 3: "Normal", + 4: "Low", + 5: "Idle", }; diff --git a/packages/scan/src/lite/create-emitter.ts b/packages/scan/src/lite/create-emitter.ts index 7b420452..13fa4baa 100644 --- a/packages/scan/src/lite/create-emitter.ts +++ b/packages/scan/src/lite/create-emitter.ts @@ -1,6 +1,6 @@ -import { DEFAULT_LOCATION } from './constants'; -import type { LaneLabelTranslator } from './lane-labels'; -import type { LiteEvent, LiteEventKind, LiteOptions } from './types'; +import { DEFAULT_LOCATION } from "./constants"; +import type { LaneLabelTranslator } from "./lane-labels"; +import type { LiteEvent, LiteEventKind, LiteOptions } from "./types"; /** * Surface used inside the lite module's hot path (event emission, listener @@ -67,8 +67,8 @@ export const createEmitter = ( if (canPostToEndpoint) { try { fetch(endpoint as string, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, location: `${locationPrefix}:${kind}`, diff --git a/packages/scan/src/lite/create-profiling-hooks.ts b/packages/scan/src/lite/create-profiling-hooks.ts index ca4a642c..897ad1d9 100644 --- a/packages/scan/src/lite/create-profiling-hooks.ts +++ b/packages/scan/src/lite/create-profiling-hooks.ts @@ -1,64 +1,60 @@ -import { type Fiber, getDisplayName } from 'bippy'; -import type { Emitter } from './create-emitter'; -import type { ProfilingHooks } from './types'; +import { type Fiber, getDisplayName } from "bippy"; +import type { Emitter } from "./create-emitter"; +import type { ProfilingHooks } from "./types"; -const componentNameOf = (fiber: Fiber): string => - getDisplayName(fiber.type) ?? 'Anonymous'; +const componentNameOf = (fiber: Fiber): string => getDisplayName(fiber.type) ?? "Anonymous"; export const createProfilingHooks = (emitter: Emitter): ProfilingHooks => ({ - markCommitStarted: (lanes) => emitter.emit('commit-start', { lanes }), - markCommitStopped: () => emitter.emit('commit-stop'), - markRenderStarted: (lanes) => emitter.emit('render-start', { lanes }), - markRenderYielded: () => emitter.emit('render-yield'), - markRenderStopped: () => emitter.emit('render-stop'), - markRenderScheduled: (lane) => emitter.emit('render-scheduled', { lanes: lane }), - markLayoutEffectsStarted: (lanes) => emitter.emit('layout-effects-start', { lanes }), - markLayoutEffectsStopped: () => emitter.emit('layout-effects-stop'), - markPassiveEffectsStarted: (lanes) => - emitter.emit('passive-effects-start', { lanes }), - markPassiveEffectsStopped: () => emitter.emit('passive-effects-stop'), + markCommitStarted: (lanes) => emitter.emit("commit-start", { lanes }), + markCommitStopped: () => emitter.emit("commit-stop"), + markRenderStarted: (lanes) => emitter.emit("render-start", { lanes }), + markRenderYielded: () => emitter.emit("render-yield"), + markRenderStopped: () => emitter.emit("render-stop"), + markRenderScheduled: (lane) => emitter.emit("render-scheduled", { lanes: lane }), + markLayoutEffectsStarted: (lanes) => emitter.emit("layout-effects-start", { lanes }), + markLayoutEffectsStopped: () => emitter.emit("layout-effects-stop"), + markPassiveEffectsStarted: (lanes) => emitter.emit("passive-effects-start", { lanes }), + markPassiveEffectsStopped: () => emitter.emit("passive-effects-stop"), markComponentRenderStarted: (fiber) => - emitter.emit('component-render-start', { componentName: componentNameOf(fiber) }), - markComponentRenderStopped: () => emitter.emit('component-render-stop'), + emitter.emit("component-render-start", { componentName: componentNameOf(fiber) }), + markComponentRenderStopped: () => emitter.emit("component-render-stop"), markComponentLayoutEffectMountStarted: (fiber) => - emitter.emit('component-layout-effect-mount-start', { + emitter.emit("component-layout-effect-mount-start", { componentName: componentNameOf(fiber), }), - markComponentLayoutEffectMountStopped: () => - emitter.emit('component-layout-effect-mount-stop'), + markComponentLayoutEffectMountStopped: () => emitter.emit("component-layout-effect-mount-stop"), markComponentLayoutEffectUnmountStarted: (fiber) => - emitter.emit('component-layout-effect-unmount-start', { + emitter.emit("component-layout-effect-unmount-start", { componentName: componentNameOf(fiber), }), markComponentLayoutEffectUnmountStopped: () => - emitter.emit('component-layout-effect-unmount-stop'), + emitter.emit("component-layout-effect-unmount-stop"), markComponentPassiveEffectMountStarted: (fiber) => - emitter.emit('component-passive-effect-mount-start', { + emitter.emit("component-passive-effect-mount-start", { componentName: componentNameOf(fiber), }), - markComponentPassiveEffectMountStopped: () => - emitter.emit('component-passive-effect-mount-stop'), + markComponentPassiveEffectMountStopped: () => emitter.emit("component-passive-effect-mount-stop"), markComponentPassiveEffectUnmountStarted: (fiber) => - emitter.emit('component-passive-effect-unmount-start', { + emitter.emit("component-passive-effect-unmount-start", { componentName: componentNameOf(fiber), }), markComponentPassiveEffectUnmountStopped: () => - emitter.emit('component-passive-effect-unmount-stop'), + emitter.emit("component-passive-effect-unmount-stop"), markStateUpdateScheduled: (fiber, lane) => - emitter.emit('state-update', { componentName: componentNameOf(fiber), lanes: lane }), + emitter.emit("state-update", { componentName: componentNameOf(fiber), lanes: lane }), markForceUpdateScheduled: (fiber, lane) => - emitter.emit('force-update', { componentName: componentNameOf(fiber), lanes: lane }), + emitter.emit("force-update", { componentName: componentNameOf(fiber), lanes: lane }), markComponentSuspended: (fiber, _wakeable, lanes) => - emitter.emit('component-suspended', { + emitter.emit("component-suspended", { componentName: componentNameOf(fiber), lanes, }), markComponentErrored: (fiber, thrownValue, lanes) => { const message = - thrownValue && typeof thrownValue === 'object' && 'message' in thrownValue + thrownValue && typeof thrownValue === "object" && "message" in thrownValue ? String((thrownValue as { message: unknown }).message) : String(thrownValue); - emitter.emit('component-errored', { + emitter.emit("component-errored", { componentName: componentNameOf(fiber), lanes, message, diff --git a/packages/scan/src/lite/fiber-source.ts b/packages/scan/src/lite/fiber-source.ts index 09ef6b25..cae47567 100644 --- a/packages/scan/src/lite/fiber-source.ts +++ b/packages/scan/src/lite/fiber-source.ts @@ -1,11 +1,5 @@ -import { type Fiber, getDisplayName } from 'bippy'; -import { - type FiberSource, - formatOwnerStack, - hasDebugSource, - hasDebugStack, - parseStack, -} from 'bippy/source'; +import { type Fiber, getDisplayName } from "bippy"; +import { type FiberSource, formatOwnerStack, hasDebugStack, parseStack } from "bippy/source"; /** * Synchronous source extraction. We deliberately avoid `bippy/source`'s @@ -19,22 +13,23 @@ import { * element. Bundled URLs only; callers must symbolicate offline. */ export const getFiberSource = (fiber: Fiber): FiberSource | null => { - // `hasDebugSource` narrows `_debugSource` to NonNullable, so direct access - // is safe. Same for `_debugStack` via `hasDebugStack`. Both guards live in - // `bippy/source` because the underlying fields are version-dependent and - // bippy is the canonical place to know what shape they take. + // Reading `_debugSource` into a local narrows it to NonNullable, so direct + // access is safe. `_debugStack` is narrowed via `hasDebugStack`, whose guard + // lives in `bippy/source` because the underlying field is version-dependent + // and bippy is the canonical place to know what shape it takes. // - // ASSUMPTION: bippy's guards are tight — `hasDebugSource(fiber) === true` - // implies `fiber._debugSource.fileName` is a `string` and `lineNumber` is - // a `number` (verified in bippy 0.5.39). If a future bippy loosens this + // ASSUMPTION: bippy's Fiber type keeps `_debugSource.fileName` as a `string` + // and `lineNumber` as a `number` whenever `_debugSource` is present + // (verified in bippy 0.6.0). If a future bippy loosens this // (e.g. narrows to `fileName?: string`), the resulting `FiberSource` would // violate `bippy/source`'s `FiberSource.fileName: string` contract. Re-check // this file when bumping bippy. - if (hasDebugSource(fiber)) { + const debugSource = fiber._debugSource; + if (debugSource) { return { - fileName: fiber._debugSource.fileName, - lineNumber: fiber._debugSource.lineNumber, - columnNumber: fiber._debugSource.columnNumber, + fileName: debugSource.fileName, + lineNumber: debugSource.lineNumber, + columnNumber: debugSource.columnNumber, }; } diff --git a/packages/scan/src/lite/lane-labels.ts b/packages/scan/src/lite/lane-labels.ts index 23439f75..e18dc16d 100644 --- a/packages/scan/src/lite/lane-labels.ts +++ b/packages/scan/src/lite/lane-labels.ts @@ -1,5 +1,5 @@ -import { REACT_TOTAL_NUM_LANES, SCHEDULER_PRIORITY_NAMES } from './constants'; -import type { Lanes, ReactRendererWithProfiling } from './types'; +import { REACT_TOTAL_NUM_LANES, SCHEDULER_PRIORITY_NAMES } from "./constants"; +import type { Lanes, ReactRendererWithProfiling } from "./types"; export interface LaneLabelTranslator { laneLabels: (lanes: Lanes | undefined) => Array | undefined; @@ -41,7 +41,7 @@ export const createLaneLabelTranslator = ( ): LaneLabelTranslatorResult => { let laneToLabel: Map | null = null; for (const renderer of renderers) { - if (typeof renderer.getLaneLabelMap !== 'function') continue; + if (typeof renderer.getLaneLabelMap !== "function") continue; try { const map = renderer.getLaneLabelMap(); if (map && map.size > 0) { diff --git a/packages/scan/src/lite/lite.test.ts b/packages/scan/src/lite/lite.test.ts index f6ba24e0..15c387cf 100644 --- a/packages/scan/src/lite/lite.test.ts +++ b/packages/scan/src/lite/lite.test.ts @@ -1,22 +1,22 @@ -import { execFileSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { instrument } from './index'; -import type { LiteEvent } from './types'; +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { instrument } from "./index"; +import type { LiteEvent } from "./types"; // HACK: vitest runs from the package root; relying on process.cwd() avoids // import.meta (which requires module: esnext in tsconfig). const SCAN_PACKAGE_DIR = process.cwd(); -const CJS_DIST = path.join(SCAN_PACKAGE_DIR, 'dist', 'lite', 'index.js'); -const ESM_DIST = path.join(SCAN_PACKAGE_DIR, 'dist', 'lite', 'index.mjs'); +const CJS_DIST = path.join(SCAN_PACKAGE_DIR, "dist", "lite", "index.js"); +const ESM_DIST = path.join(SCAN_PACKAGE_DIR, "dist", "lite", "index.mjs"); const runInNode = (code: string): string => - execFileSync('node', ['-e', code], { + execFileSync("node", ["-e", code], { cwd: SCAN_PACKAGE_DIR, - encoding: 'utf-8', + encoding: "utf-8", timeout: 10_000, }).trim(); @@ -36,7 +36,7 @@ const stashGlobal = (key: string): { restore: () => void } => { }; const withDeletedWindow = (fn: () => T): T => { - const stash = stashGlobal('window'); + const stash = stashGlobal("window"); delete (globalThis as { window?: unknown }).window; try { return fn(); @@ -45,20 +45,20 @@ const withDeletedWindow = (fn: () => T): T => { } }; -describe('react-scan/lite SSR safety (in-process)', () => { - it('returns a noop handle when window is undefined', () => { +describe("react-scan/lite SSR safety (in-process)", () => { + it("returns a noop handle when window is undefined", () => { withDeletedWindow(() => { const handle = instrument({ - endpoint: 'http://example.test/ingest', - sessionId: 'abc', + endpoint: "http://example.test/ingest", + sessionId: "abc", }); expect(handle.isActive()).toBe(false); - expect(typeof handle.stop).toBe('function'); - expect(typeof handle.subscribe).toBe('function'); + expect(typeof handle.stop).toBe("function"); + expect(typeof handle.subscribe).toBe("function"); }); }); - it('all noop handle methods are callable without throwing', () => { + it("all noop handle methods are callable without throwing", () => { withDeletedWindow(() => { const handle = instrument({ onEvent: () => {} }); const unsubscribe = handle.subscribe(() => {}); @@ -69,10 +69,10 @@ describe('react-scan/lite SSR safety (in-process)', () => { }); }); - it('multiple instrument() calls in SSR all return noop handles', () => { + it("multiple instrument() calls in SSR all return noop handles", () => { withDeletedWindow(() => { const a = instrument(); - const b = instrument({ endpoint: 'http://example.test', sessionId: 'x' }); + const b = instrument({ endpoint: "http://example.test", sessionId: "x" }); const c = instrument(); expect(a.isActive()).toBe(false); expect(b.isActive()).toBe(false); @@ -80,8 +80,8 @@ describe('react-scan/lite SSR safety (in-process)', () => { }); }); - it('does not touch document, fetch, navigator, or XMLHttpRequest', () => { - const guards = ['document', 'fetch', 'navigator', 'XMLHttpRequest'] as const; + it("does not touch document, fetch, navigator, or XMLHttpRequest", () => { + const guards = ["document", "fetch", "navigator", "XMLHttpRequest"] as const; const stashes = guards.map((name) => stashGlobal(name)); for (const name of guards) { Object.defineProperty(globalThis, name, { @@ -94,8 +94,8 @@ describe('react-scan/lite SSR safety (in-process)', () => { try { withDeletedWindow(() => { const handle = instrument({ - endpoint: 'http://example.test', - sessionId: 'abc', + endpoint: "http://example.test", + sessionId: "abc", onEvent: () => {}, }); handle.subscribe(() => {})(); @@ -108,7 +108,7 @@ describe('react-scan/lite SSR safety (in-process)', () => { }); }); -describe('react-scan/lite happy path (with stubbed window + hook)', () => { +describe("react-scan/lite happy path (with stubbed window + hook)", () => { let stashedWindow: { restore: () => void }; let stashedHook: { restore: () => void }; let stashedReactScanLite: { restore: () => void }; @@ -126,9 +126,9 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const noopFn = (): void => {}; beforeEach(() => { - stashedWindow = stashGlobal('window'); - stashedHook = stashGlobal('__REACT_DEVTOOLS_GLOBAL_HOOK__'); - stashedReactScanLite = stashGlobal('__REACT_SCAN_LITE__'); + stashedWindow = stashGlobal("window"); + stashedHook = stashGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__"); + stashedReactScanLite = stashGlobal("__REACT_SCAN_LITE__"); (globalThis as { window?: unknown }).window = globalThis; fakeHook = { renderers: new Map(), @@ -177,7 +177,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { return { current: parent }; }; - it('emits a commit event with a populated tree on hook.onCommitFiberRoot', () => { + it("emits a commit event with a populated tree on hook.onCommitFiberRoot", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event) }); expect(handle.isActive()).toBe(true); @@ -185,25 +185,25 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - const commitEvent = events.find((event) => event.kind === 'commit'); + const commitEvent = events.find((event) => event.kind === "commit"); expect(commitEvent).toBeDefined(); expect(commitEvent?.rendererId).toBe(1); expect(commitEvent?.tree?.length).toBe(2); - expect(commitEvent?.tree?.[0]?.name).toBe('ParentComponent'); - expect(commitEvent?.tree?.[1]?.name).toBe('LeafComponent'); + expect(commitEvent?.tree?.[0]?.name).toBe("ParentComponent"); + expect(commitEvent?.tree?.[1]?.name).toBe("LeafComponent"); expect(commitEvent?.tree?.[1]?.depth).toBe(1); handle.stop(); }); - it('subscribe() listener receives commit events; unsubscribe stops them', () => { + it("subscribe() listener receives commit events; unsubscribe stops them", () => { const events: Array = []; const handle = instrument(); const unsubscribe = handle.subscribe((event) => events.push(event)); const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - expect(events.some((event) => event.kind === 'commit')).toBe(true); + expect(events.some((event) => event.kind === "commit")).toBe(true); unsubscribe(); events.length = 0; @@ -213,7 +213,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('stop() restores hook handlers and prevents further events', () => { + it("stop() restores hook handlers and prevents further events", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event) }); const ourCommit = fakeHook.onCommitFiberRoot; @@ -226,10 +226,10 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - expect(events.filter((event) => event.kind === 'commit')).toEqual([]); + expect(events.filter((event) => event.kind === "commit")).toEqual([]); }); - it('stop()/instrument() cycles do not leak chain layers', () => { + it("stop()/instrument() cycles do not leak chain layers", () => { const settler = instrument(); settler.stop(); const baselineCommit = fakeHook.onCommitFiberRoot; @@ -244,21 +244,21 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { } }); - it('stop() is idempotent', () => { + it("stop() is idempotent", () => { const handle = instrument(); handle.stop(); expect(() => handle.stop()).not.toThrow(); expect(handle.isActive()).toBe(false); }); - it('returns the existing handle if instrument() is called twice without stop()', () => { + it("returns the existing handle if instrument() is called twice without stop()", () => { const first = instrument(); const second = instrument(); expect(second).toBe(first); first.stop(); }); - it('respects maxFibersPerCommit cap', () => { + it("respects maxFibersPerCommit cap", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -268,13 +268,13 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - const commitEvent = events.find((event) => event.kind === 'commit'); + const commitEvent = events.find((event) => event.kind === "commit"); expect(commitEvent?.tree?.length).toBe(1); handle.stop(); }); - it('respects minFiberActualDurationMs threshold', () => { + it("respects minFiberActualDurationMs threshold", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -284,26 +284,22 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - const commitEvent = events.find((event) => event.kind === 'commit'); + const commitEvent = events.find((event) => event.kind === "commit"); expect(commitEvent?.tree?.length).toBe(1); - expect(commitEvent?.tree?.[0]?.name).toBe('ParentComponent'); + expect(commitEvent?.tree?.[0]?.name).toBe("ParentComponent"); handle.stop(); }); - it('warns when endpoint is provided without sessionId', () => { + it("warns when endpoint is provided without sessionId", () => { const warnings: Array = []; // oxlint-disable-next-line no-console const originalWarn = console.warn; // oxlint-disable-next-line no-console console.warn = (...args: Array) => warnings.push(args); try { - const handle = instrument({ endpoint: 'http://example.test' }); - expect( - warnings.some((entry) => - String(entry).includes('endpoint'), - ), - ).toBe(true); + const handle = instrument({ endpoint: "http://example.test" }); + expect(warnings.some((entry) => String(entry).includes("endpoint"))).toBe(true); handle.stop(); } finally { // oxlint-disable-next-line no-console @@ -311,26 +307,26 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { } }); - it('emits profiling-hooks-status with available=false when renderer has no injectProfilingHooks', () => { + it("emits profiling-hooks-status with available=false when renderer has no injectProfilingHooks", () => { const events: Array = []; - fakeHook.renderers.set(1, { version: '18.3.0', bundleType: 1 }); + fakeHook.renderers.set(1, { version: "18.3.0", bundleType: 1 }); const handle = instrument({ onEvent: (event) => events.push(event) }); - const status = events.find((event) => event.kind === 'profiling-hooks-status'); + const status = events.find((event) => event.kind === "profiling-hooks-status"); expect(status).toBeDefined(); expect(status?.available).toBe(false); - expect(status?.reason).toBe('no-inject-method'); - expect(status?.reactVersion).toBe('18.3.0'); + expect(status?.reason).toBe("no-inject-method"); + expect(status?.reactVersion).toBe("18.3.0"); expect(status?.bundleType).toBe(1); handle.stop(); }); - it('emits profiling-hooks-status with available=true when injectProfilingHooks succeeds', () => { + it("emits profiling-hooks-status with available=true when injectProfilingHooks succeeds", () => { const events: Array = []; let capturedHooks: unknown = null; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, injectProfilingHooks: (hooks: unknown) => { capturedHooks = hooks; @@ -338,7 +334,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { }); const handle = instrument({ onEvent: (event) => events.push(event) }); - const status = events.find((event) => event.kind === 'profiling-hooks-status'); + const status = events.find((event) => event.kind === "profiling-hooks-status"); expect(status?.available).toBe(true); expect(status?.reason).toBeUndefined(); expect(capturedHooks).toBeTruthy(); @@ -346,37 +342,37 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('emits profiling-hooks-status with reason=threw when injectProfilingHooks throws', () => { + it("emits profiling-hooks-status with reason=threw when injectProfilingHooks throws", () => { const events: Array = []; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, injectProfilingHooks: () => { - throw new Error('boom'); + throw new Error("boom"); }, }); const handle = instrument({ onEvent: (event) => events.push(event) }); - const status = events.find((event) => event.kind === 'profiling-hooks-status'); + const status = events.find((event) => event.kind === "profiling-hooks-status"); expect(status?.available).toBe(false); - expect(status?.reason).toBe('threw'); + expect(status?.reason).toBe("threw"); handle.stop(); }); - it('translates lanes bitmask via getLaneLabelMap', () => { + it("translates lanes bitmask via getLaneLabelMap", () => { const events: Array = []; interface CapturedHooks { markCommitStarted: (lanes: number) => void; } const captured: { hooks: CapturedHooks | null } = { hooks: null }; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, getLaneLabelMap: () => new Map([ - [1, 'SyncLane'], - [16, 'DefaultLane'], + [1, "SyncLane"], + [16, "DefaultLane"], ]), injectProfilingHooks: (hooks: CapturedHooks) => { captured.hooks = hooks; @@ -385,18 +381,18 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const handle = instrument({ onEvent: (event) => events.push(event) }); captured.hooks?.markCommitStarted(0b10001); - const commitStart = events.find((event) => event.kind === 'commit-start'); - expect(commitStart?.laneLabels).toEqual(['SyncLane', 'DefaultLane']); + const commitStart = events.find((event) => event.kind === "commit-start"); + expect(commitStart?.laneLabels).toEqual(["SyncLane", "DefaultLane"]); handle.stop(); }); - it('translates priorityLevel to priorityName on commit events', () => { + it("translates priorityLevel to priorityName on commit events", () => { const events: Array = []; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, - getLaneLabelMap: () => new Map([[1, 'SyncLane']]), + getLaneLabelMap: () => new Map([[1, "SyncLane"]]), injectProfilingHooks: () => {}, }); const handle = instrument({ onEvent: (event) => events.push(event) }); @@ -404,14 +400,14 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 2); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); expect(commit?.priorityLevel).toBe(2); - expect(commit?.priorityName).toBe('UserBlocking'); + expect(commit?.priorityName).toBe("UserBlocking"); handle.stop(); }); - it('attaches fiberId when includeFiberIdentity is true', () => { + it("attaches fiberId when includeFiberIdentity is true", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -428,15 +424,15 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - const commits = events.filter((event) => event.kind === 'commit'); - expect(commits[1]?.tree?.[0]?.fiberId).toBeTypeOf('number'); + const commits = events.filter((event) => event.kind === "commit"); + expect(commits[1]?.tree?.[0]?.fiberId).toBeTypeOf("number"); expect(commits[1]?.tree?.[0]?.fiberId).toBe(commits[2]?.tree?.[0]?.fiberId); expect(commits[1]?.tree?.[1]?.fiberId).toBe(commits[2]?.tree?.[1]?.fiberId); handle.stop(); }); - it('attaches changeDescription when recordChangeDescriptions is true', () => { + it("attaches changeDescription when recordChangeDescriptions is true", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -446,7 +442,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); const summary = commit?.tree?.[0]; expect(summary?.changeDescription).toBeDefined(); expect(summary?.changeDescription?.isFirstMount).toBe(true); @@ -454,7 +450,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('changeDescription.parent reflects whether a composite ancestor rendered', () => { + it("changeDescription.parent reflects whether a composite ancestor rendered", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -525,9 +521,9 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { fakeHook.onCommitFiberRoot?.(1, buildUpdatedTree(), 0); - const commit = events.find((event) => event.kind === 'commit'); - const parentSummary = commit?.tree?.find((entry) => entry.name === 'ParentComponent'); - const leafSummary = commit?.tree?.find((entry) => entry.name === 'LeafComponent'); + const commit = events.find((event) => event.kind === "commit"); + const parentSummary = commit?.tree?.find((entry) => entry.name === "ParentComponent"); + const leafSummary = commit?.tree?.find((entry) => entry.name === "LeafComponent"); expect(parentSummary?.changeDescription?.parent).toBe(false); expect(leafSummary?.changeDescription?.parent).toBe(true); @@ -535,7 +531,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('attaches source and ownerName when includeFiberSource is true', () => { + it("attaches source and ownerName when includeFiberSource is true", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -557,7 +553,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { treeBaseDuration: 5, _debugOwner: ownerFiber, _debugSource: { - fileName: 'src/foo.tsx', + fileName: "src/foo.tsx", lineNumber: 42, columnNumber: 3, }, @@ -565,19 +561,19 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { }; fakeHook.onCommitFiberRoot?.(1, fakeRoot, 0); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); const summary = commit?.tree?.[0]; expect(summary?.source).toEqual({ - fileName: 'src/foo.tsx', + fileName: "src/foo.tsx", lineNumber: 42, columnNumber: 3, }); - expect(summary?.ownerName).toBe('OwnerComponent'); + expect(summary?.ownerName).toBe("OwnerComponent"); handle.stop(); }); - it('source/ownerName are null when includeFiberSource is true but the fiber has no debug info', () => { + it("source/ownerName are null when includeFiberSource is true but the fiber has no debug info", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event), @@ -586,7 +582,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { fakeHook.onCommitFiberRoot?.(1, buildFakeFiberTree(), 0); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); const summary = commit?.tree?.[0]; expect(summary?.source).toBeNull(); expect(summary?.ownerName).toBeNull(); @@ -594,10 +590,10 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('priorityName resolves even when no renderer exposes getLaneLabelMap', () => { + it("priorityName resolves even when no renderer exposes getLaneLabelMap", () => { const events: Array = []; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, // no getLaneLabelMap, no injectProfilingHooks }); @@ -606,9 +602,9 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { const fakeRoot = buildFakeFiberTree(); fakeHook.onCommitFiberRoot?.(1, fakeRoot, 3); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); expect(commit?.priorityLevel).toBe(3); - expect(commit?.priorityName).toBe('Normal'); + expect(commit?.priorityName).toBe("Normal"); expect(commit?.laneLabels).toBeUndefined(); handle.stop(); @@ -617,10 +613,10 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { it('emits "opted-out" reason when includeProfilingHooks is false', () => { const events: Array = []; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, injectProfilingHooks: () => { - throw new Error('should not be called when opted out'); + throw new Error("should not be called when opted out"); }, }); const handle = instrument({ @@ -628,20 +624,20 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { includeProfilingHooks: false, }); - const status = events.find((event) => event.kind === 'profiling-hooks-status'); + const status = events.find((event) => event.kind === "profiling-hooks-status"); expect(status?.available).toBe(false); - expect(status?.reason).toBe('opted-out'); + expect(status?.reason).toBe("opted-out"); handle.stop(); }); - it('does not throw a TDZ error when a renderer is already injected', () => { + it("does not throw a TDZ error when a renderer is already injected", () => { // H2 regression: bippy may fire `onActive` synchronously inside // `getRDTHook(...)` if a renderer was already injected. Our previous // code referenced the not-yet-bound `hook` const inside that callback. // The fix is to acquire the hook first, then attach explicitly. fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, injectProfilingHooks: () => {}, }); @@ -652,7 +648,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { }).not.toThrow(); }); - it('forwards onCommitFiberRoot to the previously installed handler', () => { + it("forwards onCommitFiberRoot to the previously installed handler", () => { // M5: prove the chain forwarding actually invokes the previous handler. const calls: Array<{ rendererId: number; didError: boolean | undefined }> = []; fakeHook.onCommitFiberRoot = ( @@ -672,7 +668,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('captures and emits didError on commit events', () => { + it("captures and emits didError on commit events", () => { // H1: bippy's onCommitFiberRoot type omits the 4th `didError` arg, but // React passes it. We widen locally and emit it. const events: Array = []; @@ -686,44 +682,44 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { ) => void; widenedHandler(1, buildFakeFiberTree(), 2, true); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); expect(commit?.didError).toBe(true); handle.stop(); }); - it('omits didError on commit events when React did not pass it', () => { + it("omits didError on commit events when React did not pass it", () => { const events: Array = []; const handle = instrument({ onEvent: (event) => events.push(event) }); fakeHook.onCommitFiberRoot?.(1, buildFakeFiberTree(), 2); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); expect(commit?.didError).toBeUndefined(); handle.stop(); }); - it('emits the underlying error message when injectProfilingHooks throws', () => { + it("emits the underlying error message when injectProfilingHooks throws", () => { // M3: error message should propagate so debug agents can attribute the failure. const events: Array = []; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, injectProfilingHooks: () => { - throw new Error('renderer rejected hooks'); + throw new Error("renderer rejected hooks"); }, }); const handle = instrument({ onEvent: (event) => events.push(event) }); - const status = events.find((event) => event.kind === 'profiling-hooks-status'); - expect(status?.reason).toBe('threw'); - expect(status?.error).toBe('renderer rejected hooks'); + const status = events.find((event) => event.kind === "profiling-hooks-status"); + expect(status?.reason).toBe("threw"); + expect(status?.error).toBe("renderer rejected hooks"); handle.stop(); }); - it('warns when includeFiberTree is false but enrichment options are set', () => { + it("warns when includeFiberTree is false but enrichment options are set", () => { // H3: silent ignore is the worst failure mode. const warnings: Array = []; // oxlint-disable-next-line no-console @@ -735,9 +731,9 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { includeFiberTree: false, recordChangeDescriptions: true, }); - expect( - warnings.some((entry) => String(entry).includes('includeFiberTree: false')), - ).toBe(true); + expect(warnings.some((entry) => String(entry).includes("includeFiberTree: false"))).toBe( + true, + ); handle.stop(); } finally { // oxlint-disable-next-line no-console @@ -745,7 +741,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { } }); - it('logs an error when endpoint is not a valid http(s) URL', () => { + it("logs an error when endpoint is not a valid http(s) URL", () => { // S2: validate URL once at instrument() time instead of letting fetch fail // silently per-event. const errors: Array = []; @@ -755,14 +751,10 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { console.error = (...args: Array) => errors.push(args); try { const handle = instrument({ - endpoint: 'javascript:alert(1)', - sessionId: 'abc', + endpoint: "javascript:alert(1)", + sessionId: "abc", }); - expect( - errors.some((entry) => - String(entry).includes('not a valid http(s) URL'), - ), - ).toBe(true); + expect(errors.some((entry) => String(entry).includes("not a valid http(s) URL"))).toBe(true); handle.stop(); } finally { // oxlint-disable-next-line no-console @@ -770,16 +762,14 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { } }); - it('invalid endpoint does NOT trigger fetch on every emitted event', () => { + it("invalid endpoint does NOT trigger fetch on every emitted event", () => { // Bugbot regression on PR #435: previously the URL validator only logged // a warning, but `canPostToEndpoint` still saw the bad endpoint and // fired `fetch()` per event. Now `instrument()` clears the endpoint when // invalid before forwarding to `createEmitter`. const fetchCalls: Array = []; const originalFetch = (globalThis as { fetch?: unknown }).fetch; - (globalThis as { fetch: (url: unknown) => Promise }).fetch = ( - url, - ) => { + (globalThis as { fetch: (url: unknown) => Promise }).fetch = (url) => { fetchCalls.push(url); return Promise.resolve({}); }; @@ -789,8 +779,8 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { console.error = () => {}; try { const handle = instrument({ - endpoint: 'javascript:alert(1)', - sessionId: 'abc', + endpoint: "javascript:alert(1)", + sessionId: "abc", }); fakeHook.onCommitFiberRoot?.(1, buildFakeFiberTree(), 0); expect(fetchCalls).toEqual([]); @@ -806,15 +796,15 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { } }); - it('emit fast-path skips translator + listener work when nothing is listening', () => { + it("emit fast-path skips translator + listener work when nothing is listening", () => { // L5: when no onEvent + no endpoint + no subscribe, emit short-circuits. let translatorCalls = 0; fakeHook.renderers.set(1, { - version: '18.3.0', + version: "18.3.0", bundleType: 1, getLaneLabelMap: () => { translatorCalls++; - return new Map([[1, 'SyncLane']]); + return new Map([[1, "SyncLane"]]); }, injectProfilingHooks: () => {}, }); @@ -830,7 +820,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('cascade detection rejects fibers whose actualDuration is 0', () => { + it("cascade detection rejects fibers whose actualDuration is 0", () => { // L6: a parent that didn't actually render must not be reported as cascading. const events: Array = []; const handle = instrument({ @@ -898,19 +888,19 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { fakeHook.onCommitFiberRoot?.(1, { current: parent }, 0); - const commit = events.find((event) => event.kind === 'commit'); - const leafSummary = commit?.tree?.find((entry) => entry.name === 'LeafComponent'); + const commit = events.find((event) => event.kind === "commit"); + const leafSummary = commit?.tree?.find((entry) => entry.name === "LeafComponent"); expect(leafSummary?.changeDescription?.parent).toBe(false); handle.stop(); }); - it('does not double-attach a renderer across stop()/instrument() cycles', () => { + it("does not double-attach a renderer across stop()/instrument() cycles", () => { // L7: WeakSet of attached renderers must survive across cycles for the // same renderer instance. const injects: Array = []; const renderer = { - version: '18.3.0', + version: "18.3.0", bundleType: 1, injectProfilingHooks: () => { injects.push(1); @@ -931,7 +921,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('detects context value changes via traverseContexts', () => { + it("detects context value changes via traverseContexts", () => { // M4: covers `didAnyContextChange`. const events: Array = []; const handle = instrument({ @@ -939,7 +929,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { recordChangeDescriptions: true, }); - const contextRef = { displayName: 'TestContext' }; + const contextRef = { displayName: "TestContext" }; const previousFiber = { tag: 0, type: function Consumer() {}, @@ -954,7 +944,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { selfBaseDuration: 5, treeBaseDuration: 5, dependencies: { - firstContext: { context: contextRef, memoizedValue: 'before', next: null }, + firstContext: { context: contextRef, memoizedValue: "before", next: null }, }, }; const fiber = { @@ -971,20 +961,20 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { selfBaseDuration: 5, treeBaseDuration: 5, dependencies: { - firstContext: { context: contextRef, memoizedValue: 'after', next: null }, + firstContext: { context: contextRef, memoizedValue: "after", next: null }, }, }; fakeHook.onCommitFiberRoot?.(1, { current: fiber }, 0); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); const summary = commit?.tree?.[0]; expect(summary?.changeDescription?.context).toBe(true); handle.stop(); }); - it('detects class state changes via shallow key compare', () => { + it("detects class state changes via shallow key compare", () => { // M4: covers `didAnyClassStateChange`. Tag 1 = ClassComponentTag. const events: Array = []; const handle = instrument({ @@ -1023,7 +1013,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { fakeHook.onCommitFiberRoot?.(1, { current: fiber }, 0); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); const summary = commit?.tree?.[0]; expect(summary?.changeDescription?.state).toBe(true); expect(summary?.changeDescription?.hooks).toEqual([]); @@ -1031,7 +1021,7 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { handle.stop(); }); - it('detects hook-state changes via traverseState', () => { + it("detects hook-state changes via traverseState", () => { // M4: covers `collectChangedHookIndices`. Function components only. const events: Array = []; const handle = instrument({ @@ -1039,9 +1029,9 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { recordChangeDescriptions: true, }); - const prevHook2 = { memoizedState: 'b', next: null }; + const prevHook2 = { memoizedState: "b", next: null }; const prevHook1 = { memoizedState: 1, next: prevHook2 }; - const nextHook2 = { memoizedState: 'b', next: null }; + const nextHook2 = { memoizedState: "b", next: null }; const nextHook1 = { memoizedState: 2, next: nextHook2 }; const previousFiber = { @@ -1075,20 +1065,20 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { fakeHook.onCommitFiberRoot?.(1, { current: fiber }, 0); - const commit = events.find((event) => event.kind === 'commit'); + const commit = events.find((event) => event.kind === "commit"); const summary = commit?.tree?.[0]; expect(summary?.changeDescription?.hooks).toEqual([0]); handle.stop(); }); - it('handle.stop() called from inside an onEvent listener takes effect', () => { + it("handle.stop() called from inside an onEvent listener takes effect", () => { // M6: realistic self-disabling instrumentation pattern. let commitCount = 0; let handleRef: { stop: () => void } | null = null; handleRef = instrument({ onEvent: (event) => { - if (event.kind === 'commit') { + if (event.kind === "commit") { commitCount++; handleRef?.stop(); } @@ -1104,45 +1094,39 @@ describe('react-scan/lite happy path (with stubbed window + hook)', () => { }); }); -describe.skipIf(!existsSync(CJS_DIST))( - 'react-scan/lite SSR safety (built CJS in node -e)', - () => { - it('importing the CJS build does not throw', () => { - expect(runInNode(`require('${CJS_DIST}'); console.log('OK')`)).toBe('OK'); - }); +describe.skipIf(!existsSync(CJS_DIST))("react-scan/lite SSR safety (built CJS in node -e)", () => { + it("importing the CJS build does not throw", () => { + expect(runInNode(`require('${CJS_DIST}'); console.log('OK')`)).toBe("OK"); + }); - it('instrument() returns a noop handle in node -e', () => { - const code = [ - `const { instrument } = require('${CJS_DIST}');`, - "const handle = instrument({ endpoint: 'http://example.test', sessionId: 'abc' });", - "console.log(handle.isActive() === false ? 'NOOP' : 'UNEXPECTED');", - ].join(' '); - expect(runInNode(code)).toBe('NOOP'); - }); - }, -); - -describe.skipIf(!existsSync(ESM_DIST))( - 'react-scan/lite SSR safety (built ESM in node -e)', - () => { - it('importing the ESM build does not throw', () => { - const url = pathToFileURL(ESM_DIST).href; - expect( - runInNode( - `import('${url}').then(() => console.log('OK')).catch(e => { console.error(e); process.exit(1); })`, - ), - ).toBe('OK'); - }); + it("instrument() returns a noop handle in node -e", () => { + const code = [ + `const { instrument } = require('${CJS_DIST}');`, + "const handle = instrument({ endpoint: 'http://example.test', sessionId: 'abc' });", + "console.log(handle.isActive() === false ? 'NOOP' : 'UNEXPECTED');", + ].join(" "); + expect(runInNode(code)).toBe("NOOP"); + }); +}); - it('instrument() returns a noop handle when imported as ESM', () => { - const url = pathToFileURL(ESM_DIST).href; - const code = [ - `import('${url}').then(({ instrument }) => {`, - " const handle = instrument({ endpoint: 'http://example.test', sessionId: 'abc' });", - " console.log(handle.isActive() === false ? 'NOOP' : 'UNEXPECTED');", - '}).catch(e => { console.error(e); process.exit(1); });', - ].join(' '); - expect(runInNode(code)).toBe('NOOP'); - }); - }, -); +describe.skipIf(!existsSync(ESM_DIST))("react-scan/lite SSR safety (built ESM in node -e)", () => { + it("importing the ESM build does not throw", () => { + const url = pathToFileURL(ESM_DIST).href; + expect( + runInNode( + `import('${url}').then(() => console.log('OK')).catch(e => { console.error(e); process.exit(1); })`, + ), + ).toBe("OK"); + }); + + it("instrument() returns a noop handle when imported as ESM", () => { + const url = pathToFileURL(ESM_DIST).href; + const code = [ + `import('${url}').then(({ instrument }) => {`, + " const handle = instrument({ endpoint: 'http://example.test', sessionId: 'abc' });", + " console.log(handle.isActive() === false ? 'NOOP' : 'UNEXPECTED');", + "}).catch(e => { console.error(e); process.exit(1); });", + ].join(" "); + expect(runInNode(code)).toBe("NOOP"); + }); +}); diff --git a/packages/scan/src/lite/types.ts b/packages/scan/src/lite/types.ts index 18a1cd8d..faf6e217 100644 --- a/packages/scan/src/lite/types.ts +++ b/packages/scan/src/lite/types.ts @@ -1,5 +1,5 @@ -import type { BundleType, Fiber, Lanes, ReactRenderer } from 'bippy'; -import type { FiberSource } from 'bippy/source'; +import type { BundleType, Fiber, Lanes, ReactRenderer } from "bippy"; +import type { FiberSource } from "bippy/source"; export type { BundleType, Fiber, FiberSource, Lanes }; @@ -84,43 +84,43 @@ export interface LiteFiberSummary { } export type LiteEventKind = - | 'renderer-injected' - | 'profiling-hooks-status' - | 'commit' - | 'post-commit' - | 'fiber-unmount' - | 'commit-start' - | 'commit-stop' - | 'render-start' - | 'render-yield' - | 'render-stop' - | 'render-scheduled' - | 'layout-effects-start' - | 'layout-effects-stop' - | 'passive-effects-start' - | 'passive-effects-stop' - | 'component-render-start' - | 'component-render-stop' - | 'component-layout-effect-mount-start' - | 'component-layout-effect-mount-stop' - | 'component-layout-effect-unmount-start' - | 'component-layout-effect-unmount-stop' - | 'component-passive-effect-mount-start' - | 'component-passive-effect-mount-stop' - | 'component-passive-effect-unmount-start' - | 'component-passive-effect-unmount-stop' - | 'state-update' - | 'force-update' - | 'component-suspended' - | 'component-errored'; + | "renderer-injected" + | "profiling-hooks-status" + | "commit" + | "post-commit" + | "fiber-unmount" + | "commit-start" + | "commit-stop" + | "render-start" + | "render-yield" + | "render-stop" + | "render-scheduled" + | "layout-effects-start" + | "layout-effects-stop" + | "passive-effects-start" + | "passive-effects-stop" + | "component-render-start" + | "component-render-stop" + | "component-layout-effect-mount-start" + | "component-layout-effect-mount-stop" + | "component-layout-effect-unmount-start" + | "component-layout-effect-unmount-stop" + | "component-passive-effect-mount-start" + | "component-passive-effect-mount-stop" + | "component-passive-effect-unmount-start" + | "component-passive-effect-unmount-stop" + | "state-update" + | "force-update" + | "component-suspended" + | "component-errored"; export type ProfilingHooksUnavailableReason = /** Renderer doesn't expose `injectProfilingHooks` (R19.2+ prod, non-`__PROFILE__` builds). */ - | 'no-inject-method' + | "no-inject-method" /** `injectProfilingHooks` threw when called. */ - | 'threw' + | "threw" /** Caller passed `includeProfilingHooks: false`; we never tried to attach. */ - | 'opted-out'; + | "opted-out"; export interface LiteEvent { kind: LiteEventKind; diff --git a/packages/scan/src/lite/walk-fiber.ts b/packages/scan/src/lite/walk-fiber.ts index 944416bf..2cf21b13 100644 --- a/packages/scan/src/lite/walk-fiber.ts +++ b/packages/scan/src/lite/walk-fiber.ts @@ -1,7 +1,7 @@ -import { type Fiber, getDisplayName, getFiberId } from 'bippy'; -import { getChangeDescription, isCompositeTag } from './change-description'; -import { getFiberSource, getOwnerName } from './fiber-source'; -import type { LiteFiberSummary } from './types'; +import { type Fiber, getDisplayName, getFiberId } from "bippy"; +import { getChangeDescription, isCompositeTag } from "./change-description"; +import { getFiberSource, getOwnerName } from "./fiber-source"; +import type { LiteFiberSummary } from "./types"; export interface WalkFiberOptions { maxFibers: number; @@ -29,9 +29,7 @@ interface PendingFiber { const compositeFiberDidRender = (fiber: Fiber): boolean => { const actualDuration = fiber.actualDuration; - return ( - actualDuration != null && actualDuration > 0 && isCompositeTag(fiber.tag) - ); + return actualDuration != null && actualDuration > 0 && isCompositeTag(fiber.tag); }; export const walkFiber = ( @@ -62,7 +60,7 @@ export const walkFiber = ( const actualDuration = currentFiber.actualDuration; if (actualDuration != null && actualDuration >= options.minActualDurationMs) { const summary: LiteFiberSummary = { - name: getDisplayName(currentFiber.type) ?? 'Anonymous', + name: getDisplayName(currentFiber.type) ?? "Anonymous", depth: currentDepth, tag: currentFiber.tag, actualDuration, @@ -78,10 +76,7 @@ export const walkFiber = ( summary.ownerName = getOwnerName(currentFiber); } if (options.recordChangeDescriptions) { - summary.changeDescription = getChangeDescription( - currentFiber, - hasCascadingAncestor, - ); + summary.changeDescription = getChangeDescription(currentFiber, hasCascadingAncestor); } summaries.push(summary); } diff --git a/packages/scan/src/new-outlines/index.ts b/packages/scan/src/new-outlines/index.ts index 63df5e57..d4b617e7 100644 --- a/packages/scan/src/new-outlines/index.ts +++ b/packages/scan/src/new-outlines/index.ts @@ -9,25 +9,25 @@ import { isCompositeFiber, } from "bippy"; import { - Change, - ContextChange, - PropsChange, + type Change, + type ContextChange, + type PropsChange, ReactScanInternals, - Store, ignoredProps, -} from "~core/index"; +} from "../core/index"; +import { Store, getInspectState, getOptionsState, setLastReportTime } from "../core/native-state"; import { ChangeReason, createInstrumentation, getContextChanges, getStateChanges, OldRenderData, -} from "~core/instrumentation"; -import { log, logIntro } from "~web/utils/log"; -import { inspectorUpdateSignal } from "~web/views/inspector/states"; +} from "../core/instrumentation"; +import { getChangedPropsDetailed } from "../core/inspection/fiber"; +import { log, logIntro } from "../web/utils/log"; +import { notifyInspectorUpdate } from "../web/views/inspector/states"; import { OUTLINE_ARRAY_SIZE, drawCanvas, initCanvas, updateOutlines, updateScroll } from "./canvas"; import type { ActiveOutline, BlueprintOutline, OutlineData } from "./types"; -import { getChangedPropsDetailed } from "~web/views/inspector/utils"; // The worker code will be replaced at build time const workerCode = "__WORKER_CODE__"; @@ -273,6 +273,7 @@ const draw = () => { const IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED = typeof OffscreenCanvas !== "undefined" && typeof Worker !== "undefined"; +const HAS_BUNDLED_WORKER_CODE = workerCode !== "__WORKER_CODE__"; const getDpr = () => { return Math.min(window.devicePixelRatio || 1, 2); @@ -308,9 +309,14 @@ const getCanvasEl = () => { // Users on a strict CSP without `worker-src blob:` (see #372) can opt out; // we still render outlines on the main thread via `initCanvas` below. - const workerOptOut = ReactScanInternals.options.value.useOffscreenCanvasWorker === false; - - if (IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED && !window.__REACT_SCAN_EXTENSION__ && !workerOptOut) { + const workerOptOut = getOptionsState().useOffscreenCanvasWorker === false; + + if ( + IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED && + HAS_BUNDLED_WORKER_CODE && + !window.__REACT_SCAN_EXTENSION__ && + !workerOptOut + ) { try { const blobUrl = URL.createObjectURL( new Blob([workerCode], { type: "application/javascript" }), @@ -333,7 +339,7 @@ const getCanvasEl = () => { // The fallback below renders fine on the main thread, so log only when // the user explicitly asked for verbose diagnostics. worker = null; - if (ReactScanInternals.options.value._debug === "verbose") { + if (getOptionsState()._debug === "verbose") { // oxlint-disable-next-line no-console console.warn("Failed to initialize OffscreenCanvas worker:", error); } @@ -432,10 +438,7 @@ const cleanup = () => { const reportRenderToListeners = (fiber: Fiber) => { if (isCompositeFiber(fiber)) { // report render has a non trivial cost because it calls Date.now(), so we want to avoid the computation if possible - if ( - ReactScanInternals.options.value.showToolbar !== false && - Store.inspectState.value.kind === "focused" - ) { + if (getOptionsState().showToolbar !== false && getInspectState().kind === "focused") { const reportFiber = fiber; const { selfTime } = getTimings(fiber); const displayName = getDisplayName(fiber.type); @@ -501,7 +504,7 @@ const startReportInterval = () => { clearInterval(reportInterval); reportInterval = setInterval(() => { if (needsReport) { - Store.lastReportTime.value = Date.now(); + setLastReportTime(Date.now()); needsReport = false; } }, 50); @@ -543,7 +546,7 @@ export const initReactScanInstrumentation = (setupToolbar: () => void) => { const instrumentation = createInstrumentation("react-scan-devtools-0.1.0", { onCommitStart: () => { - ReactScanInternals.options.value.onCommitStart?.(); + getOptionsState().onCommitStart?.(); }, onActive: (() => { let didActivate = false; @@ -570,10 +573,10 @@ export const initReactScanInstrumentation = (setupToolbar: () => void) => { if (isCompositeFiber(fiber)) { Store.interactionListeningForRenders?.(fiber, renders); } - const isOverlayPaused = ReactScanInternals.instrumentation?.isPaused.value; + const isOverlayPaused = instrumentation.getIsPaused(); + const inspectState = getInspectState(); const isInspectorInactive = - Store.inspectState.value.kind === "inspect-off" || - Store.inspectState.value.kind === "uninitialized"; + inspectState.kind === "inspect-off" || inspectState.kind === "uninitialized"; const shouldFullyAbort = isOverlayPaused && isInspectorInactive; if (shouldFullyAbort) { @@ -582,23 +585,23 @@ export const initReactScanInstrumentation = (setupToolbar: () => void) => { if (!isOverlayPaused) { outlineFiber(fiber); } - if (ReactScanInternals.options.value.log) { + if (getOptionsState().log) { // this can be expensive given enough re-renders log(renders); } - if (Store.inspectState.value.kind === "focused") { - inspectorUpdateSignal.value = Date.now(); + if (inspectState.kind === "focused") { + notifyInspectorUpdate(); } if (!isInspectorInactive) { reportRenderToListeners(fiber); } - ReactScanInternals.options.value.onRender?.(fiber, renders); + getOptionsState().onRender?.(fiber, renders); }, onCommitFinish: () => { scheduleSetup(); - ReactScanInternals.options.value.onCommitFinish?.(); + getOptionsState().onCommitFinish?.(); }, onPostCommitFiberRoot() { scheduleSetup(); diff --git a/packages/scan/src/new-outlines/offscreen-canvas.worker.ts b/packages/scan/src/new-outlines/offscreen-canvas.worker.ts index 47ec93e0..30bfff12 100644 --- a/packages/scan/src/new-outlines/offscreen-canvas.worker.ts +++ b/packages/scan/src/new-outlines/offscreen-canvas.worker.ts @@ -1,5 +1,5 @@ -import { OUTLINE_ARRAY_SIZE, drawCanvas, initCanvas } from './canvas'; -import type { ActiveOutline } from './types'; +import { OUTLINE_ARRAY_SIZE, drawCanvas, initCanvas } from "./canvas"; +import type { ActiveOutline } from "./types"; let canvas: OffscreenCanvas | null = null; let ctx: OffscreenCanvasRenderingContext2D | null = null; @@ -23,7 +23,7 @@ const draw = () => { self.onmessage = (event) => { const { type } = event.data; - if (type === 'init') { + if (type === "init") { canvas = event.data.canvas; dpr = event.data.dpr; @@ -36,7 +36,7 @@ self.onmessage = (event) => { if (!canvas || !ctx) return; - if (type === 'resize') { + if (type === "resize") { dpr = event.data.dpr; canvas.width = event.data.width * dpr; canvas.height = event.data.height * dpr; @@ -47,7 +47,7 @@ self.onmessage = (event) => { return; } - if (type === 'draw-outlines') { + if (type === "draw-outlines") { const { data, names } = event.data; const sharedView = new Float32Array(data); @@ -96,7 +96,7 @@ self.onmessage = (event) => { return; } - if (type === 'scroll') { + if (type === "scroll") { const { deltaX, deltaY } = event.data; for (const outline of activeOutlines.values()) { const newX = outline.x - deltaX; diff --git a/packages/scan/src/polyfills.ts b/packages/scan/src/polyfills.ts index caf4877d..18c3eea8 100644 --- a/packages/scan/src/polyfills.ts +++ b/packages/scan/src/polyfills.ts @@ -1,9 +1,9 @@ if (!Array.prototype.toSorted) { - Object.defineProperty(Array.prototype, 'toSorted', { + Object.defineProperty(Array.prototype, "toSorted", { value: function (this: Array, compareFn?: (a: T, b: T) => number): Array { return [...this].sort(compareFn); }, writable: true, configurable: true, }); -} \ No newline at end of file +} diff --git a/packages/scan/src/react-component-name/__tests__/arrow-function.test.ts b/packages/scan/src/react-component-name/__tests__/arrow-function.test.ts index d266c253..ea290520 100644 --- a/packages/scan/src/react-component-name/__tests__/arrow-function.test.ts +++ b/packages/scan/src/react-component-name/__tests__/arrow-function.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { transform } from './utils'; +import { describe, it, expect } from "vitest"; +import { transform } from "./utils"; -describe('arrow function components', () => { - it('handles inline JSX return', async () => { +describe("arrow function components", () => { + it("handles inline JSX return", async () => { const input = ` export const Button = () => `; @@ -11,7 +11,7 @@ describe('arrow function components', () => { expect(result).toContain("Button.displayName = 'Button'"); }); - it('handles block with JSX return', async () => { + it("handles block with JSX return", async () => { const input = ` const Modal = () => { return
Modal content
@@ -21,7 +21,7 @@ describe('arrow function components', () => { expect(result).toContain("Modal.displayName = 'Modal'"); }); - it('handles conditional returns', async () => { + it("handles conditional returns", async () => { const input = ` const ConditionalComponent = ({ show }) => { if (show) { @@ -31,12 +31,10 @@ describe('arrow function components', () => { } `; const result = await transform(input); - expect(result).toContain( - "ConditionalComponent.displayName = 'ConditionalComponent'", - ); + expect(result).toContain("ConditionalComponent.displayName = 'ConditionalComponent'"); }); - it('handles early returns', async () => { + it("handles early returns", async () => { const input = ` const EarlyReturn = ({ loading, error, data }) => { if (loading) return
Loading...
diff --git a/packages/scan/src/react-component-name/__tests__/complex-patterns.test.ts b/packages/scan/src/react-component-name/__tests__/complex-patterns.test.ts index f3dafe04..555701f7 100644 --- a/packages/scan/src/react-component-name/__tests__/complex-patterns.test.ts +++ b/packages/scan/src/react-component-name/__tests__/complex-patterns.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { transform } from './utils'; +import { describe, it, expect } from "vitest"; +import { transform } from "./utils"; -describe('complex component patterns', () => { - it('handles components with hooks', async () => { +describe("complex component patterns", () => { + it("handles components with hooks", async () => { const input = ` const TodoList = () => { const [todos, setTodos] = useState([]) @@ -16,7 +16,7 @@ describe('complex component patterns', () => { expect(result).toContain("TodoList.displayName = 'TodoList'"); }); - it('handles components with multiple state updates', async () => { + it("handles components with multiple state updates", async () => { const input = ` const Counter = () => { const [count, setCount] = useState(0) @@ -35,7 +35,7 @@ describe('complex component patterns', () => { expect(result).toContain("Counter.displayName = 'Counter'"); }); - it('handles components with render props', async () => { + it("handles components with render props", async () => { const input = ` const DataFetcher = ({ children, url }) => { const [data, setData] = useState(null) @@ -49,7 +49,7 @@ describe('complex component patterns', () => { expect(result).toContain("DataFetcher.displayName = 'DataFetcher'"); }); - it('handles higher-order components', async () => { + it("handles higher-order components", async () => { const input = ` const withData = (WrappedComponent) => { const WithData = (props) => { diff --git a/packages/scan/src/react-component-name/__tests__/function-declarations.test.ts b/packages/scan/src/react-component-name/__tests__/function-declarations.test.ts index 92fab660..b51f0e48 100644 --- a/packages/scan/src/react-component-name/__tests__/function-declarations.test.ts +++ b/packages/scan/src/react-component-name/__tests__/function-declarations.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { transform } from './utils'; +import { describe, it, expect } from "vitest"; +import { transform } from "./utils"; -describe('function declarations', () => { - it('handles named function declarations', async () => { +describe("function declarations", () => { + it("handles named function declarations", async () => { const input = ` function Welcome(props) { return

Hello, {props.name}

@@ -12,7 +12,7 @@ describe('function declarations', () => { expect(result).toContain("Welcome.displayName = 'Welcome'"); }); - it('handles async components', async () => { + it("handles async components", async () => { const input = ` async function AsyncComponent({ id }) { const data = await fetchData(id) diff --git a/packages/scan/src/react-component-name/__tests__/general-cases.test.ts b/packages/scan/src/react-component-name/__tests__/general-cases.test.ts index ffabf172..e636014b 100644 --- a/packages/scan/src/react-component-name/__tests__/general-cases.test.ts +++ b/packages/scan/src/react-component-name/__tests__/general-cases.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from 'vitest'; -import { transform } from './utils'; +import { describe, expect, it } from "vitest"; +import { transform } from "./utils"; -describe('edge cases', () => { - it('handles nested component declarations', async () => { +describe("edge cases", () => { + it("handles nested component declarations", async () => { const input = ` const Parent = () => { const NestedChild = () =>
Child
@@ -18,7 +18,7 @@ describe('edge cases', () => { expect(result).toContain("NestedChild.displayName = 'NestedChild'"); }); - it('handles components with complex expressions', async () => { + it("handles components with complex expressions", async () => { const input = ` const DynamicComponent = () => { const content = useMemo(() => ( @@ -39,12 +39,10 @@ describe('edge cases', () => { } `; const result = await transform(input); - expect(result).toContain( - "DynamicComponent.displayName = 'DynamicComponent'", - ); + expect(result).toContain("DynamicComponent.displayName = 'DynamicComponent'"); }); - it('handles components with multiple returns in switch/case', async () => { + it("handles components with multiple returns in switch/case", async () => { const input = ` const StatusComponent = ({ status }) => { switch (status) { @@ -63,7 +61,7 @@ describe('edge cases', () => { expect(result).toContain("StatusComponent.displayName = 'StatusComponent'"); }); - it('handles components with try/catch blocks', async () => { + it("handles components with try/catch blocks", async () => { const input = ` const SafeComponent = () => { try { @@ -78,7 +76,7 @@ describe('edge cases', () => { expect(result).toContain("SafeComponent.displayName = 'SafeComponent'"); }); - it('handles components returning primitive values', async () => { + it("handles components returning primitive values", async () => { const input = ` // Null component const EmptyComponent = () => null; @@ -146,21 +144,15 @@ describe('edge cases', () => { // "BooleanComponent.displayName = 'BooleanComponent'", // ); expect(result).toContain("ListComponent.displayName = 'ListComponent'"); - expect(result).toContain( - "ConditionalComponent.displayName = 'ConditionalComponent'", - ); - expect(result).toContain( - "DynamicComponent.displayName = 'DynamicComponent'", - ); + expect(result).toContain("ConditionalComponent.displayName = 'ConditionalComponent'"); + expect(result).toContain("DynamicComponent.displayName = 'DynamicComponent'"); expect(result).toContain("AsyncComponent.displayName = 'AsyncComponent'"); expect(result).toContain("PortalComponent.displayName = 'PortalComponent'"); - expect(result).toContain( - "FragmentComponent.displayName = 'FragmentComponent'", - ); + expect(result).toContain("FragmentComponent.displayName = 'FragmentComponent'"); expect(result).toContain("NestedComponent.displayName = 'NestedComponent'"); }); - it('handles components with complex conditional returns', async () => { + it("handles components with complex conditional returns", async () => { const input = ` const ComplexComponent = ({ type, data }) => { switch (type) { @@ -217,29 +209,17 @@ describe('edge cases', () => { }; `; const result = await transform(input); - expect(result).toContain( - "ComplexComponent.displayName = 'ComplexComponent'", - ); - expect(result).toContain( - "TernaryComponent.displayName = 'TernaryComponent'", - ); - expect(result).toContain( - "ShortCircuitComponent.displayName = 'ShortCircuitComponent'", - ); - expect(result).toContain( - "NullishComponent.displayName = 'NullishComponent'", - ); - expect(result).toContain( - "ChainedComponent.displayName = 'ChainedComponent'", - ); + expect(result).toContain("ComplexComponent.displayName = 'ComplexComponent'"); + expect(result).toContain("TernaryComponent.displayName = 'TernaryComponent'"); + expect(result).toContain("ShortCircuitComponent.displayName = 'ShortCircuitComponent'"); + expect(result).toContain("NullishComponent.displayName = 'NullishComponent'"); + expect(result).toContain("ChainedComponent.displayName = 'ChainedComponent'"); expect(result).toContain("DataComponent.displayName = 'DataComponent'"); expect(result).toContain("SuspenseImage.displayName = 'SuspenseImage'"); - expect(result).toContain( - "ProfileComponent.displayName = 'ProfileComponent'", - ); + expect(result).toContain("ProfileComponent.displayName = 'ProfileComponent'"); }); - it('handles components with complex state and hooks', async () => { + it("handles components with complex state and hooks", async () => { const input = ` export const ValueUpdate = ({ valueUpdate, @@ -367,7 +347,7 @@ describe('edge cases', () => { expect(result).toContain("DataGrid.displayName = 'DataGrid'"); }); - it('handles all forwardRef patterns', async () => { + it("handles all forwardRef patterns", async () => { const input = ` import React from 'react'; @@ -434,7 +414,7 @@ describe('edge cases', () => { expect(result).toContain("EnhancedInput.displayName = 'EnhancedInput'"); }); - it('handles all memo patterns', async () => { + it("handles all memo patterns", async () => { const input = ` import React from 'react'; // Basic memo @@ -512,7 +492,7 @@ describe('edge cases', () => { expect(result).toContain("MemoInput.displayName = 'MemoInput'"); }); - it('handles components with various function calls returning JSX', async () => { + it("handles components with various function calls returning JSX", async () => { const input = ` const ArrayMethodsComponent = ({ items }) => { // Filter then map @@ -632,23 +612,17 @@ describe('edge cases', () => { `; const result = await transform(input); - expect(result).toContain( - "ArrayMethodsComponent.displayName = 'ArrayMethodsComponent'", - ); - expect(result).toContain( - "CustomFunctionsComponent.displayName = 'CustomFunctionsComponent'", - ); + expect(result).toContain("ArrayMethodsComponent.displayName = 'ArrayMethodsComponent'"); + expect(result).toContain("CustomFunctionsComponent.displayName = 'CustomFunctionsComponent'"); expect(result).toContain("AsyncComponent.displayName = 'AsyncComponent'"); - expect(result).toContain( - "ChainedComponent.displayName = 'ChainedComponent'", - ); + expect(result).toContain("ChainedComponent.displayName = 'ChainedComponent'"); expect(result).toContain("BaseComponent.displayName = 'BaseComponent'"); // expect(result).toContain( // "EnhancedComponent.displayName = 'EnhancedComponent'", // ); }); - it('handles shadcn-style component patterns', async () => { + it("handles shadcn-style component patterns", async () => { const input = ` import React from 'react'; // Basic shadcn component pattern @@ -745,16 +719,14 @@ describe('edge cases', () => { const result = await transform(input); expect(result).toContain("Button.displayName = 'Button'"); - expect(result).toContain( - "ButtonWithVariants.displayName = 'ButtonWithVariants'", - ); + expect(result).toContain("ButtonWithVariants.displayName = 'ButtonWithVariants'"); expect(result).toContain("Card.displayName = 'Card'"); expect(result).toContain("CardHeader.displayName = 'CardHeader'"); expect(result).toContain("Dialog.displayName = 'Dialog'"); expect(result).toContain("DialogTrigger.displayName = 'DialogTrigger'"); }); - it('handles legacy and unconventional component patterns', async () => { + it("handles legacy and unconventional component patterns", async () => { const input = ` @@ -852,22 +824,14 @@ describe('edge cases', () => { `; const result = await transform(input); - expect(result).toContain( - "CreateClassComponent.displayName = 'CreateClassComponent'", - ); + expect(result).toContain("CreateClassComponent.displayName = 'CreateClassComponent'"); expect(result).toContain("WithMixins.displayName = 'WithMixins'"); // expect(result).toContain( // "FactoryComponent.displayName = 'FactoryComponent'", // ); - expect(result).toContain( - "DecoratedComponent.displayName = 'DecoratedComponent'", - ); - expect(result).toContain( - "RenderPropComponent.displayName = 'RenderPropComponent'", - ); - expect(result).toContain( - "OldContextComponent.displayName = 'OldContextComponent'", - ); + expect(result).toContain("DecoratedComponent.displayName = 'DecoratedComponent'"); + expect(result).toContain("RenderPropComponent.displayName = 'RenderPropComponent'"); + expect(result).toContain("OldContextComponent.displayName = 'OldContextComponent'"); // expect(result).toContain("PartialButton.displayName = 'PartialButton'"); expect(result).toContain("PluginComponent.displayName = 'PluginComponent'"); // expect(result).toContain( diff --git a/packages/scan/src/react-component-name/__tests__/react-patterns.test.ts b/packages/scan/src/react-component-name/__tests__/react-patterns.test.ts index 7bba680d..24fcf526 100644 --- a/packages/scan/src/react-component-name/__tests__/react-patterns.test.ts +++ b/packages/scan/src/react-component-name/__tests__/react-patterns.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { transform } from './utils'; +import { describe, it, expect } from "vitest"; +import { transform } from "./utils"; -describe('modern React patterns', () => { - it('handles components with hooks and context', async () => { +describe("modern React patterns", () => { + it("handles components with hooks and context", async () => { const input = ` const UserProfile = () => { const { user } = useContext(UserContext) @@ -19,7 +19,7 @@ describe('modern React patterns', () => { expect(result).toContain("UserProfile.displayName = 'UserProfile'"); }); - it('handles components with custom hooks', async () => { + it("handles components with custom hooks", async () => { const input = ` const SearchResults = () => { const { data, loading, error } = useQuery(SEARCH_QUERY) @@ -41,7 +41,7 @@ describe('modern React patterns', () => { expect(result).toContain("SearchResults.displayName = 'SearchResults'"); }); - it('handles components with suspense boundaries', async () => { + it("handles components with suspense boundaries", async () => { const input = ` const AsyncContent = () => { const data = useSuspenseQuery(QUERY) @@ -56,7 +56,7 @@ describe('modern React patterns', () => { expect(result).toContain("AsyncContent.displayName = 'AsyncContent'"); }); - it('handles components with error boundaries', async () => { + it("handles components with error boundaries", async () => { const input = ` import React from 'react'; diff --git a/packages/scan/src/react-component-name/__tests__/ts-patterns.test.ts b/packages/scan/src/react-component-name/__tests__/ts-patterns.test.ts index fb1ab804..ad613315 100644 --- a/packages/scan/src/react-component-name/__tests__/ts-patterns.test.ts +++ b/packages/scan/src/react-component-name/__tests__/ts-patterns.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { transform } from './utils'; +import { describe, it, expect } from "vitest"; +import { transform } from "./utils"; -describe('typescript patterns', () => { - it('handles components with type parameters', async () => { +describe("typescript patterns", () => { + it("handles components with type parameters", async () => { const input = ` interface Props { items: T[] @@ -17,7 +17,7 @@ describe('typescript patterns', () => { expect(result).toContain("List.displayName = 'List'"); }); - it('handles components with complex types', async () => { + it("handles components with complex types", async () => { const input = ` type Props = { id: string diff --git a/packages/scan/src/react-component-name/__tests__/utils.ts b/packages/scan/src/react-component-name/__tests__/utils.ts index e90b0953..5499b1bc 100644 --- a/packages/scan/src/react-component-name/__tests__/utils.ts +++ b/packages/scan/src/react-component-name/__tests__/utils.ts @@ -1,9 +1,6 @@ -import { type Options, reactComponentNamePlugin } from '..'; +import { type Options, reactComponentNamePlugin } from ".."; -type TransformFn = ( - code: string, - id: string, -) => Promise<{ code: string } | string | null>; +type TransformFn = (code: string, id: string) => Promise<{ code: string } | string | null>; export const transform = async (code: string, options?: Options) => { const plugin = reactComponentNamePlugin.vite(options || {}) as { @@ -18,10 +15,10 @@ export const transform = async (code: string, options?: Options) => { error: console.error, }, code, - 'test.tsx', + "test.tsx", ); if (!result) return code; - if (typeof result === 'string') return result; + if (typeof result === "string") return result; return result.code; }; diff --git a/packages/scan/src/react-component-name/astro.ts b/packages/scan/src/react-component-name/astro.ts index f8ca1cef..1b81af82 100644 --- a/packages/scan/src/react-component-name/astro.ts +++ b/packages/scan/src/react-component-name/astro.ts @@ -1,11 +1,11 @@ -import type { Options } from '.'; -import vite from './vite'; +import type { Options } from "."; +import vite from "./vite"; export default (options: Options = {}) => ({ - name: 'react-component-name', + name: "react-component-name", hooks: { // oxlint-disable-next-line typescript/no-explicit-any - 'astro:config:setup': (astro: any) => { + "astro:config:setup": (astro: any) => { astro.config.vite.plugins ||= []; astro.config.vite.plugins.push(vite(options)); }, diff --git a/packages/scan/src/react-component-name/babel/index.ts b/packages/scan/src/react-component-name/babel/index.ts index ae43fcb8..96ec107f 100644 --- a/packages/scan/src/react-component-name/babel/index.ts +++ b/packages/scan/src/react-component-name/babel/index.ts @@ -1,9 +1,9 @@ -import type { NodePath, PluginObj } from '@babel/core'; -import * as t from '@babel/types'; -import type { Options } from '../core/options'; -import { isComponentishName } from './is-componentish-name'; -import { pathReferencesImport } from './path-references-import'; -import { unwrapNode, unwrapPath } from './unwrap'; +import type { NodePath, PluginObj } from "@babel/core"; +import * as t from "@babel/types"; +import type { Options } from "../core/options"; +import { isComponentishName } from "./is-componentish-name"; +import { pathReferencesImport } from "./path-references-import"; +import { unwrapNode, unwrapPath } from "./unwrap"; function getAssignedDisplayNames(path: NodePath): Set { const names = new Set(); @@ -19,10 +19,7 @@ function getAssignedDisplayNames(path: NodePath): Set { if (!object) { return; } - if ( - t.isIdentifier(memberExpr.property) && - memberExpr.property.name === 'displayName' - ) { + if (t.isIdentifier(memberExpr.property) && memberExpr.property.name === "displayName") { names.add(object.name); } }, @@ -30,9 +27,7 @@ function getAssignedDisplayNames(path: NodePath): Set { return names; } -function isValidFunction( - node: t.Node, -): node is t.ArrowFunctionExpression | t.FunctionExpression { +function isValidFunction(node: t.Node): node is t.ArrowFunctionExpression | t.FunctionExpression { return t.isArrowFunctionExpression(node) || t.isFunctionExpression(node); } @@ -45,8 +40,8 @@ function assignDisplayName( statement.insertAfter([ t.expressionStatement( t.assignmentExpression( - '=', - t.memberExpression(t.identifier(name), t.identifier('displayName')), + "=", + t.memberExpression(t.identifier(name), t.identifier("displayName")), t.stringLiteral(name), ), ), @@ -57,22 +52,19 @@ function assignDisplayName( t.blockStatement([ t.expressionStatement( t.assignmentExpression( - '=', - t.memberExpression( - t.identifier(name), - t.identifier('displayName'), - ), + "=", + t.memberExpression(t.identifier(name), t.identifier("displayName")), t.stringLiteral(name), ), ), ]), - t.catchClause(t.identifier('error'), t.blockStatement([])), + t.catchClause(t.identifier("error"), t.blockStatement([])), ), ]); } } -const REACT_CLASS = ['Component', 'PureComponent']; +const REACT_CLASS = ["Component", "PureComponent"]; function isNamespaceExport( namespace: string, @@ -85,28 +77,26 @@ function isNamespaceExport( } const memberExpr = unwrapPath(path, t.isMemberExpression); if (memberExpr) { - const object = unwrapPath(memberExpr.get('object'), t.isIdentifier); + const object = unwrapPath(memberExpr.get("object"), t.isIdentifier); if (object && object.node.name === namespace) { - const property = memberExpr.get('property'); - return ( - property.isIdentifier() && moduleExports.includes(property.node.name) - ); + const property = memberExpr.get("property"); + return property.isIdentifier() && moduleExports.includes(property.node.name); } } return false; } function isReactClassComponent(path: NodePath): boolean { - const superClass = path.get('superClass'); + const superClass = path.get("superClass"); if (!superClass.isExpression()) { return false; } - if (isNamespaceExport('React', REACT_CLASS, superClass)) { + if (isNamespaceExport("React", REACT_CLASS, superClass)) { return true; } // The usual - if (pathReferencesImport(superClass, 'react', REACT_CLASS, false, true)) { + if (pathReferencesImport(superClass, "react", REACT_CLASS, false, true)) { return true; } return false; @@ -119,13 +109,13 @@ function isStyledComponent( ): boolean { function isStyledImport(path: NodePath): boolean { return ( - (path.isIdentifier() && path.node.name === 'styled') || + (path.isIdentifier() && path.node.name === "styled") || pathReferencesImport(path, moduleName, importName, false, false) ); } const callExpr = unwrapPath(path, t.isCallExpression); if (callExpr) { - const callee = callExpr.get('callee'); + const callee = callExpr.get("callee"); // styled('h1', () => {...}); if (isStyledImport(callee)) { return true; @@ -133,7 +123,7 @@ function isStyledComponent( // styled.h1(() => {...}) const memberExpr = unwrapPath(callee, t.isMemberExpression); if (memberExpr) { - const object = unwrapPath(memberExpr.get('object'), t.isIdentifier); + const object = unwrapPath(memberExpr.get("object"), t.isIdentifier); if (object && isStyledImport(object)) { return true; } @@ -144,11 +134,11 @@ function isStyledComponent( const taggedExpr = unwrapPath(path, t.isTaggedTemplateExpression); if (taggedExpr) { - const tag = taggedExpr.get('tag'); + const tag = taggedExpr.get("tag"); const memberExpr = unwrapPath(tag, t.isMemberExpression); if (memberExpr) { - const object = unwrapPath(memberExpr.get('object'), t.isIdentifier); + const object = unwrapPath(memberExpr.get("object"), t.isIdentifier); // styled.h1`...`; if (object && isStyledImport(object)) { return true; @@ -160,7 +150,7 @@ function isStyledComponent( // styled(Link)`...` const callExpr = unwrapPath(tag, t.isCallExpression); if (callExpr) { - const callee = callExpr.get('callee'); + const callee = callExpr.get("callee"); if (isStyledImport(callee)) { return true; } @@ -172,16 +162,13 @@ function isStyledComponent( } const REACT_FACTORY = [ - 'forwardRef', - 'memo', - 'createClass', + "forwardRef", + "memo", + "createClass", // 'lazy', ]; -function isReactComponent( - expr: NodePath, - flags: Options['flags'], -): boolean { +function isReactComponent(expr: NodePath, flags: Options["flags"]): boolean { // Check for class components const classExpr = unwrapPath(expr, t.isClassExpression); if (classExpr && isReactClassComponent(classExpr)) { @@ -195,22 +182,21 @@ function isReactComponent( // Time for call exprs const callExpr = unwrapPath(expr, t.isCallExpression); if (callExpr) { - const callee = callExpr.get('callee'); + const callee = callExpr.get("callee"); // React const factory = [...REACT_FACTORY]; if (!flags?.noCreateContext) { - factory.push('createContext'); + factory.push("createContext"); } if ( - (callee.isExpression() && - isNamespaceExport('React', REACT_FACTORY, callee)) || - pathReferencesImport(callee, 'react', REACT_FACTORY, false, true) + (callee.isExpression() && isNamespaceExport("React", REACT_FACTORY, callee)) || + pathReferencesImport(callee, "react", REACT_FACTORY, false, true) ) { return true; } const identifier = unwrapPath(callee, t.isIdentifier); if (identifier) { - if (identifier.node.name === 'createReactClass') { + if (identifier.node.name === "createReactClass") { return true; } // Assume HOCs @@ -221,17 +207,17 @@ function isReactComponent( } if (flags?.noStyledComponents) return false; - if (isStyledComponent('@emotion/styled', ['default'], expr)) { + if (isStyledComponent("@emotion/styled", ["default"], expr)) { return true; } - if (isStyledComponent('styled-components', ['default'], expr)) { + if (isStyledComponent("styled-components", ["default"], expr)) { return true; } return false; } export const reactScanComponentNamePlugin = (options?: Options): PluginObj => ({ - name: 'react-scan/component-name', + name: "react-scan/component-name", visitor: { Program(path) { const assignedNames = getAssignedDisplayNames(path); @@ -276,7 +262,7 @@ export const reactScanComponentNamePlugin = (options?: Options): PluginObj => ({ return; } const identifier = path.node.id; - const init = path.get('init'); + const init = path.get("init"); if (!(init.isExpression() && t.isIdentifier(identifier))) { return; } @@ -287,11 +273,7 @@ export const reactScanComponentNamePlugin = (options?: Options): PluginObj => ({ const name = identifier.name; if (!assignedNames.has(name)) { - assignDisplayName( - path.parentPath, - name, - options?.flags?.noTryCatchDisplayNames, - ); + assignDisplayName(path.parentPath, name, options?.flags?.noTryCatchDisplayNames); } } }, diff --git a/packages/scan/src/react-component-name/babel/is-componentish-name.ts b/packages/scan/src/react-component-name/babel/is-componentish-name.ts index bb9a53bb..30b8a5ff 100644 --- a/packages/scan/src/react-component-name/babel/is-componentish-name.ts +++ b/packages/scan/src/react-component-name/babel/is-componentish-name.ts @@ -1,15 +1,13 @@ // This is just a Pascal heuristic // we only assume a function is a component -import type { Options } from '../core/options'; +import type { Options } from "../core/options"; // if the first character is in uppercase -export function isComponentishName(name: string, flags: Options['flags']) { +export function isComponentishName(name: string, flags: Options["flags"]) { return ( - name[0] >= 'A' && - name[0] <= 'Z' && - !flags?.ignoreComponentSubstrings?.some((substring) => - name.includes(substring), - ) + name[0] >= "A" && + name[0] <= "Z" && + !flags?.ignoreComponentSubstrings?.some((substring) => name.includes(substring)) ); } diff --git a/packages/scan/src/react-component-name/babel/is-nested-expression.ts b/packages/scan/src/react-component-name/babel/is-nested-expression.ts index d0eefe1d..cb1a0c53 100644 --- a/packages/scan/src/react-component-name/babel/is-nested-expression.ts +++ b/packages/scan/src/react-component-name/babel/is-nested-expression.ts @@ -1,4 +1,4 @@ -import type * as t from '@babel/types'; +import type * as t from "@babel/types"; type NestedExpression = | t.ParenthesizedExpression | t.TypeCastExpression @@ -10,13 +10,13 @@ type NestedExpression = export const isNestedExpression = (node: t.Node): node is NestedExpression => { switch (node.type) { - case 'ParenthesizedExpression': - case 'TypeCastExpression': - case 'TSAsExpression': - case 'TSSatisfiesExpression': - case 'TSNonNullExpression': - case 'TSTypeAssertion': - case 'TSInstantiationExpression': + case "ParenthesizedExpression": + case "TypeCastExpression": + case "TSAsExpression": + case "TSSatisfiesExpression": + case "TSNonNullExpression": + case "TSTypeAssertion": + case "TSInstantiationExpression": return true; default: return false; diff --git a/packages/scan/src/react-component-name/babel/is-path-valid.ts b/packages/scan/src/react-component-name/babel/is-path-valid.ts index ce0c3886..49a780be 100644 --- a/packages/scan/src/react-component-name/babel/is-path-valid.ts +++ b/packages/scan/src/react-component-name/babel/is-path-valid.ts @@ -1,5 +1,5 @@ -import type { NodePath } from '@babel/core'; -import type * as t from '@babel/types'; +import type { NodePath } from "@babel/core"; +import type * as t from "@babel/types"; type TypeFilter = (node: t.Node) => node is V; diff --git a/packages/scan/src/react-component-name/babel/path-references-import.ts b/packages/scan/src/react-component-name/babel/path-references-import.ts index 0ae869ba..dbb387c2 100644 --- a/packages/scan/src/react-component-name/babel/path-references-import.ts +++ b/packages/scan/src/react-component-name/babel/path-references-import.ts @@ -1,7 +1,7 @@ -import type { NodePath } from '@babel/core'; -import * as t from '@babel/types'; -import { isPathValid } from './is-path-valid'; -import { unwrapPath } from './unwrap'; +import type { NodePath } from "@babel/core"; +import * as t from "@babel/types"; +import { isPathValid } from "./is-path-valid"; +import { unwrapPath } from "./unwrap"; export const pathReferencesImport = ( path: NodePath, @@ -13,7 +13,7 @@ export const pathReferencesImport = ( const identifier = unwrapPath(path, t.isIdentifier); if (identifier) { const binding = path.scope.getBinding(identifier.node.name); - if (binding && binding.kind === 'module') { + if (binding && binding.kind === "module") { const importPath = binding.path; const importParent = importPath.parentPath; if ( @@ -27,38 +27,35 @@ export const pathReferencesImport = ( return importName.includes(key); } if (isPathValid(importPath, t.isImportDefaultSpecifier)) { - return importName.includes('default'); + return importName.includes("default"); } if (isPathValid(importPath, t.isImportNamespaceSpecifier)) { - return importName.includes('*'); + return importName.includes("*"); } } } return false; } const memberExpr = - unwrapPath(path, t.isMemberExpression) || - unwrapPath(path, t.isOptionalMemberExpression); + unwrapPath(path, t.isMemberExpression) || unwrapPath(path, t.isOptionalMemberExpression); if (memberExpr) { - const object = unwrapPath(memberExpr.get('object'), t.isIdentifier); + const object = unwrapPath(memberExpr.get("object"), t.isIdentifier); if (!object) { return false; } - const property = memberExpr.get('property'); + const property = memberExpr.get("property"); if (isPathValid(property, t.isIdentifier)) { return ( importName.includes(property.node.name) && - (pathReferencesImport(object, moduleSource, ['*'], asType) || - (defaultNamespace && - pathReferencesImport(object, moduleSource, ['default'], asType))) + (pathReferencesImport(object, moduleSource, ["*"], asType) || + (defaultNamespace && pathReferencesImport(object, moduleSource, ["default"], asType))) ); } if (isPathValid(property, t.isStringLiteral)) { return ( importName.includes(property.node.value) && - (pathReferencesImport(object, moduleSource, ['*'], asType) || - (defaultNamespace && - pathReferencesImport(object, moduleSource, ['default'], asType))) + (pathReferencesImport(object, moduleSource, ["*"], asType) || + (defaultNamespace && pathReferencesImport(object, moduleSource, ["default"], asType))) ); } } diff --git a/packages/scan/src/react-component-name/babel/unwrap.ts b/packages/scan/src/react-component-name/babel/unwrap.ts index 9fde3552..cbaf5887 100644 --- a/packages/scan/src/react-component-name/babel/unwrap.ts +++ b/packages/scan/src/react-component-name/babel/unwrap.ts @@ -1,7 +1,7 @@ -import type { NodePath } from '@babel/core'; -import type * as t from '@babel/types'; -import { isNestedExpression } from './is-nested-expression'; -import { isPathValid } from './is-path-valid'; +import type { NodePath } from "@babel/core"; +import type * as t from "@babel/types"; +import { isNestedExpression } from "./is-nested-expression"; +import { isPathValid } from "./is-path-valid"; type TrueTypeFilter = (node: t.Node) => node is U; type TypeCheck = K extends TrueTypeFilter ? U : never; @@ -34,7 +34,7 @@ export const unwrapPath = ( return path; } if (isPathValid(path, isNestedExpression)) { - return unwrapPath(path.get('expression'), key); + return unwrapPath(path.get("expression"), key); } return undefined; }; diff --git a/packages/scan/src/react-component-name/esbuild.ts b/packages/scan/src/react-component-name/esbuild.ts index 17328961..f255acdc 100644 --- a/packages/scan/src/react-component-name/esbuild.ts +++ b/packages/scan/src/react-component-name/esbuild.ts @@ -1,3 +1,3 @@ -import { reactComponentNamePlugin } from '.' +import { reactComponentNamePlugin } from "."; -export default reactComponentNamePlugin.esbuild +export default reactComponentNamePlugin.esbuild; diff --git a/packages/scan/src/react-component-name/index.ts b/packages/scan/src/react-component-name/index.ts index b7fffd26..1838b379 100644 --- a/packages/scan/src/react-component-name/index.ts +++ b/packages/scan/src/react-component-name/index.ts @@ -1,8 +1,8 @@ -import { transformAsync } from '@babel/core'; -import { createFilter } from '@rollup/pluginutils'; -import { createUnplugin } from 'unplugin'; -import { reactScanComponentNamePlugin } from './babel'; -import type { Options } from './core/options'; +import { transformAsync } from "@babel/core"; +import { createFilter } from "@rollup/pluginutils"; +import { createUnplugin } from "unplugin"; +import { reactScanComponentNamePlugin } from "./babel"; +import type { Options } from "./core/options"; export const transform = async ( code: string, @@ -17,7 +17,7 @@ export const transform = async ( plugins: [reactScanComponentNamePlugin(options)], ignore: [/\/(?build|node_modules)\//], parserOpts: { - plugins: ['jsx', 'typescript', 'decorators'], + plugins: ["jsx", "typescript", "decorators"], }, cloneInputAst: false, filename: id, @@ -28,51 +28,49 @@ export const transform = async ( babelrc: false, generatorOpts: { jsescOption: { - quotes: 'single', + quotes: "single", minimal: true, }, }, }); if (result?.code) { - return { code: result.code ?? '', map: result.map }; + return { code: result.code ?? "", map: result.map }; } return null; } catch (error) { // oxlint-disable-next-line no-console - console.error('Error processing file:', id, error); + console.error("Error processing file:", id, error); return null; } }; -export const DEFAULT_INCLUDE = '**/*.{mtsx,mjsx,tsx,jsx}'; -export const DEFAULT_EXCLUDE = '**/node_modules/**'; -export const reactComponentNamePlugin = createUnplugin( - (options?: Options) => { - // mirror to loader.ts when changing this - const filter = createFilter( - options?.include || DEFAULT_INCLUDE, - options?.exclude || [ - DEFAULT_EXCLUDE, - // Next.js pages dir specific - '**/_app.{jsx,tsx,js,ts}', - '**/_document.{jsx,tsx,js,ts}', - '**/api/**/*', - // Million.js specific - '**/.million/**/*', - ], - ); +export const DEFAULT_INCLUDE = "**/*.{mtsx,mjsx,tsx,jsx}"; +export const DEFAULT_EXCLUDE = "**/node_modules/**"; +export const reactComponentNamePlugin = createUnplugin((options?: Options) => { + // mirror to loader.ts when changing this + const filter = createFilter( + options?.include || DEFAULT_INCLUDE, + options?.exclude || [ + DEFAULT_EXCLUDE, + // Next.js pages dir specific + "**/_app.{jsx,tsx,js,ts}", + "**/_document.{jsx,tsx,js,ts}", + "**/api/**/*", + // Million.js specific + "**/.million/**/*", + ], + ); - return { - name: 'react-component-name', - enforce: 'post', - async transform(code, id) { - return transform(code, id, filter, options); - }, - }; - }, -); + return { + name: "react-component-name", + enforce: "post", + async transform(code, id) { + return transform(code, id, filter, options); + }, + }; +}); export default reactComponentNamePlugin; export type { Options }; diff --git a/packages/scan/src/react-component-name/loader.ts b/packages/scan/src/react-component-name/loader.ts index 3c25a59c..bb87461b 100644 --- a/packages/scan/src/react-component-name/loader.ts +++ b/packages/scan/src/react-component-name/loader.ts @@ -1,14 +1,10 @@ -import { type FilterPattern, createFilter } from '@rollup/pluginutils'; -import { DEFAULT_EXCLUDE, DEFAULT_INCLUDE, transform } from '.'; +import { type FilterPattern, createFilter } from "@rollup/pluginutils"; +import { DEFAULT_EXCLUDE, DEFAULT_INCLUDE, transform } from "."; interface LoaderContext { getOptions(): { include?: FilterPattern; exclude?: FilterPattern }; resourcePath: string; - async(): ( - error: Error | null, - content?: string, - sourceMap?: string | object, - ) => void; + async(): (error: Error | null, content?: string, sourceMap?: string | object) => void; } export default async function ReactComponentNameLoader( @@ -16,8 +12,7 @@ export default async function ReactComponentNameLoader( code: string, sourceMap: string | object | undefined, ) { - const parsedMap = - typeof sourceMap === 'string' ? JSON.parse(sourceMap) : sourceMap; + const parsedMap = typeof sourceMap === "string" ? JSON.parse(sourceMap) : sourceMap; const callback = this.async(); try { const options = this.getOptions(); @@ -27,22 +22,18 @@ export default async function ReactComponentNameLoader( options?.exclude || [ DEFAULT_EXCLUDE, // Next.js pages dir specific - '**/_app.{jsx,tsx,js,ts}', - '**/_document.{jsx,tsx,js,ts}', - '**/api/**/*', + "**/_app.{jsx,tsx,js,ts}", + "**/_document.{jsx,tsx,js,ts}", + "**/api/**/*", // Million.js specific - '**/.million/**/*', + "**/.million/**/*", ], ); if (!filter(id)) return callback(null, code, parsedMap); const result = await transform(code, id, filter); - callback( - null, - result?.code || '', - result?.map ? JSON.stringify(result.map) : undefined, - ); + callback(null, result?.code || "", result?.map ? JSON.stringify(result.map) : undefined); } catch (e) { callback(e as Error); } diff --git a/packages/scan/src/react-component-name/rolldown.ts b/packages/scan/src/react-component-name/rolldown.ts index 11b27fdc..33a42344 100644 --- a/packages/scan/src/react-component-name/rolldown.ts +++ b/packages/scan/src/react-component-name/rolldown.ts @@ -1,4 +1,3 @@ import { reactComponentNamePlugin } from "."; - export default reactComponentNamePlugin.rolldown; diff --git a/packages/scan/src/react-component-name/rollup.ts b/packages/scan/src/react-component-name/rollup.ts index 97b728aa..6d6d93a8 100644 --- a/packages/scan/src/react-component-name/rollup.ts +++ b/packages/scan/src/react-component-name/rollup.ts @@ -1,3 +1,3 @@ -import reactComponentNamePlugin from '.'; +import reactComponentNamePlugin from "."; export default reactComponentNamePlugin.rollup; diff --git a/packages/scan/src/react-component-name/rspack.ts b/packages/scan/src/react-component-name/rspack.ts index ddc5a6d1..070c2a65 100644 --- a/packages/scan/src/react-component-name/rspack.ts +++ b/packages/scan/src/react-component-name/rspack.ts @@ -1,3 +1,3 @@ -import reactComponentNamePlugin from '.'; +import reactComponentNamePlugin from "."; export default reactComponentNamePlugin.rspack; diff --git a/packages/scan/src/react-component-name/tsconfig.json b/packages/scan/src/react-component-name/tsconfig.json index 8845da38..f548dca1 100644 --- a/packages/scan/src/react-component-name/tsconfig.json +++ b/packages/scan/src/react-component-name/tsconfig.json @@ -12,5 +12,5 @@ "skipDefaultLibCheck": true, "types": ["@types/node"] }, - "include": ["."], + "include": ["."] } diff --git a/packages/scan/src/react-component-name/vite.ts b/packages/scan/src/react-component-name/vite.ts index 461407e4..3ca57cf2 100644 --- a/packages/scan/src/react-component-name/vite.ts +++ b/packages/scan/src/react-component-name/vite.ts @@ -1,3 +1,3 @@ -import { reactComponentNamePlugin } from '.'; +import { reactComponentNamePlugin } from "."; export default reactComponentNamePlugin.vite; diff --git a/packages/scan/src/react-component-name/webpack.ts b/packages/scan/src/react-component-name/webpack.ts index 3753916a..c57df6ec 100644 --- a/packages/scan/src/react-component-name/webpack.ts +++ b/packages/scan/src/react-component-name/webpack.ts @@ -1,3 +1,3 @@ -import { reactComponentNamePlugin } from '.'; +import { reactComponentNamePlugin } from "."; export default reactComponentNamePlugin.webpack; diff --git a/packages/scan/src/rsc-shim.ts b/packages/scan/src/rsc-shim.ts new file mode 100644 index 00000000..f9b04875 --- /dev/null +++ b/packages/scan/src/rsc-shim.ts @@ -0,0 +1,14 @@ +export type * from "./core"; + +export const Store = undefined; +export const ReactScanInternals = undefined; +export const getReport = () => {}; +export const setOptions = () => {}; +export const getOptions = () => {}; +export const getIsProduction = () => {}; +export const start = () => {}; +export const scan = () => {}; +export const useScan = () => {}; +export const onRender = () => {}; +export const ignoredProps = new WeakSet(); +export const ignoreScan = () => {}; diff --git a/packages/scan/src/runtime/create-scan-runtime.ts b/packages/scan/src/runtime/create-scan-runtime.ts new file mode 100644 index 00000000..ec7b8b6e --- /dev/null +++ b/packages/scan/src/runtime/create-scan-runtime.ts @@ -0,0 +1,66 @@ +import { createRoot, onCleanup } from "solid-js"; +import { startTimingTracking } from "../core/notifications/event-tracking"; +import { createHighlightCanvas } from "../core/notifications/outline-overlay"; +import { createRootContainer, type RootContainerController } from "./root-container"; + +export interface ScanRuntimeOptions { + showToolbar: boolean; + isVerbose: () => boolean; +} + +let disposeActiveRuntime: (() => void) | null = null; + +const createNotificationsOutlineCanvas = (isVerbose: () => boolean) => { + try { + return createHighlightCanvas(document.documentElement); + } catch (error) { + if (isVerbose()) { + console.error( + "[React Scan Internal Error]", + "Failed to create notifications outline canvas", + error, + ); + } + } +}; + +export const disposeScanRuntime = (): void => { + disposeActiveRuntime?.(); + disposeActiveRuntime = null; + window.reactScanCleanupListeners = undefined; +}; + +export const createScanRuntime = (options: ScanRuntimeOptions): void => { + disposeScanRuntime(); + + const disposeRuntime = createRoot((dispose) => { + let isDisposed = false; + let rootController: RootContainerController | null = null; + let disposeToolbar: (() => void) | null = null; + const stopTimingTracking = startTimingTracking(); + const stopOutlineCanvas = createNotificationsOutlineCanvas(options.isVerbose); + + onCleanup(() => { + isDisposed = true; + disposeToolbar?.(); + rootController?.dispose(); + stopTimingTracking(); + stopOutlineCanvas?.(); + window.__REACT_SCAN_TOOLBAR_CONTAINER__ = undefined; + }); + + if (options.showToolbar) { + rootController = createRootContainer(); + void import("../web/toolbar").then(({ createToolbar }) => { + if (isDisposed || !rootController) return; + const toolbar = createToolbar(rootController.shadowRoot); + disposeToolbar = toolbar.dispose; + }); + } + + return dispose; + }); + + disposeActiveRuntime = disposeRuntime; + window.reactScanCleanupListeners = disposeRuntime; +}; diff --git a/packages/scan/src/runtime/root-container.ts b/packages/scan/src/runtime/root-container.ts new file mode 100644 index 00000000..35d7da76 --- /dev/null +++ b/packages/scan/src/runtime/root-container.ts @@ -0,0 +1,28 @@ +import styles from "../web/assets/css/styles.css"; +import { watchAppTheme } from "../web/utils/watch-app-theme"; + +export interface RootContainerController { + shadowRoot: ShadowRoot; + dispose: () => void; +} + +export const createRootContainer = (): RootContainerController => { + const host = document.createElement("div"); + host.id = "react-scan-root"; + + const shadowRoot = host.attachShadow({ mode: "open" }); + const styleElement = document.createElement("style"); + styleElement.textContent = styles; + shadowRoot.appendChild(styleElement); + document.documentElement.appendChild(host); + + const stopWatchingTheme = watchAppTheme(host); + + return { + shadowRoot, + dispose: () => { + stopWatchingTheme(); + host.remove(); + }, + }; +}; diff --git a/packages/scan/src/types.ts b/packages/scan/src/types.ts index bac578e7..989b8940 100644 --- a/packages/scan/src/types.ts +++ b/packages/scan/src/types.ts @@ -28,10 +28,13 @@ declare global { interface Window { reactScan: Scan; + reactScanCleanupListeners?: () => void; __REACT_SCAN_TOOLBAR_CONTAINER__?: HTMLDivElement; __REACT_SCAN_VERSION__?: string; __REACT_SCAN_EXTENSION__?: boolean; __REACT_GRAB__?: unknown; + hideIntro?: boolean; + reactScanIdCounter?: number; __REACT_DEVTOOLS_GLOBAL_HOOK__?: { checkDCE: (fn: unknown) => void; supportsFiber: boolean; diff --git a/packages/scan/src/web/utils/check-react-grab-version.ts b/packages/scan/src/utils/check-react-grab-version.ts similarity index 100% rename from packages/scan/src/web/utils/check-react-grab-version.ts rename to packages/scan/src/utils/check-react-grab-version.ts diff --git a/packages/scan/src/utils/is-client.ts b/packages/scan/src/utils/is-client.ts new file mode 100644 index 00000000..de55a62e --- /dev/null +++ b/packages/scan/src/utils/is-client.ts @@ -0,0 +1 @@ +export const IS_CLIENT = typeof window !== "undefined"; diff --git a/packages/scan/src/web/utils/is-finite-non-negative.ts b/packages/scan/src/utils/is-finite-non-negative.ts similarity index 54% rename from packages/scan/src/web/utils/is-finite-non-negative.ts rename to packages/scan/src/utils/is-finite-non-negative.ts index 17fbd8f2..8fee455e 100644 --- a/packages/scan/src/web/utils/is-finite-non-negative.ts +++ b/packages/scan/src/utils/is-finite-non-negative.ts @@ -1,2 +1,2 @@ export const isFiniteNonNegative = (value: unknown): value is number => - typeof value === 'number' && Number.isFinite(value) && value >= 0; + typeof value === "number" && Number.isFinite(value) && value >= 0; diff --git a/packages/scan/src/utils/is-plain-object.ts b/packages/scan/src/utils/is-plain-object.ts new file mode 100644 index 00000000..622eaea0 --- /dev/null +++ b/packages/scan/src/utils/is-plain-object.ts @@ -0,0 +1,2 @@ +export const isPlainObject = (value: unknown): value is Record => + Boolean(value) && typeof value === "object" && !Array.isArray(value); diff --git a/packages/scan/src/web/utils/parse-safe-area-option.test.ts b/packages/scan/src/utils/parse-safe-area-option.test.ts similarity index 53% rename from packages/scan/src/web/utils/parse-safe-area-option.test.ts rename to packages/scan/src/utils/parse-safe-area-option.test.ts index 0fa1d9e4..038e86aa 100644 --- a/packages/scan/src/web/utils/parse-safe-area-option.test.ts +++ b/packages/scan/src/utils/parse-safe-area-option.test.ts @@ -1,25 +1,25 @@ -import { describe, expect, it } from 'vitest'; -import { parseSafeAreaOption } from '~web/utils/parse-safe-area-option'; +import { describe, expect, it } from "vitest"; +import { parseSafeAreaOption } from "./parse-safe-area-option"; -describe('parseSafeAreaOption', () => { - it('accepts a non-negative finite number', () => { +describe("parseSafeAreaOption", () => { + it("accepts a non-negative finite number", () => { expect(parseSafeAreaOption(0)).toEqual({ ok: true, value: 0 }); expect(parseSafeAreaOption(24)).toEqual({ ok: true, value: 24 }); expect(parseSafeAreaOption(96)).toEqual({ ok: true, value: 96 }); }); - it('rejects negative numbers', () => { + it("rejects negative numbers", () => { const result = parseSafeAreaOption(-1); expect(result.ok).toBe(false); }); - it('rejects NaN and Infinity', () => { + it("rejects NaN and Infinity", () => { expect(parseSafeAreaOption(Number.NaN).ok).toBe(false); expect(parseSafeAreaOption(Number.POSITIVE_INFINITY).ok).toBe(false); expect(parseSafeAreaOption(Number.NEGATIVE_INFINITY).ok).toBe(false); }); - it('accepts a partial per-edge object', () => { + it("accepts a partial per-edge object", () => { expect(parseSafeAreaOption({ right: 96 })).toEqual({ ok: true, value: { right: 96 }, @@ -30,43 +30,42 @@ describe('parseSafeAreaOption', () => { }); }); - it('accepts a full per-edge object', () => { - expect( - parseSafeAreaOption({ top: 1, right: 2, bottom: 3, left: 4 }), - ).toEqual({ + it("accepts a full per-edge object", () => { + expect(parseSafeAreaOption({ top: 1, right: 2, bottom: 3, left: 4 })).toEqual({ ok: true, value: { top: 1, right: 2, bottom: 3, left: 4 }, }); }); - it('rejects per-edge object with a negative value', () => { + it("rejects per-edge object with a negative value", () => { const result = parseSafeAreaOption({ right: -10 }); expect(result.ok).toBe(false); }); - it('rejects per-edge object with a non-number value', () => { - const result = parseSafeAreaOption({ top: '24' }); + it("rejects per-edge object with a non-number value", () => { + const result = parseSafeAreaOption({ top: "24" }); expect(result.ok).toBe(false); }); - it('rejects arrays (not plain objects)', () => { + it("rejects arrays (not plain objects)", () => { const result = parseSafeAreaOption([10, 20, 30, 40]); expect(result.ok).toBe(false); }); - it('rejects null and undefined', () => { + it("rejects null and undefined", () => { expect(parseSafeAreaOption(null).ok).toBe(false); expect(parseSafeAreaOption(undefined).ok).toBe(false); }); - it('rejects strings', () => { - expect(parseSafeAreaOption('24').ok).toBe(false); - expect(parseSafeAreaOption('').ok).toBe(false); + it("rejects strings", () => { + expect(parseSafeAreaOption("24").ok).toBe(false); + expect(parseSafeAreaOption("").ok).toBe(false); }); - it('ignores undefined edges in a per-edge object', () => { - expect( - parseSafeAreaOption({ top: 5, right: undefined }), - ).toEqual({ ok: true, value: { top: 5 } }); + it("ignores undefined edges in a per-edge object", () => { + expect(parseSafeAreaOption({ top: 5, right: undefined })).toEqual({ + ok: true, + value: { top: 5 }, + }); }); }); diff --git a/packages/scan/src/web/utils/parse-safe-area-option.ts b/packages/scan/src/utils/parse-safe-area-option.ts similarity index 69% rename from packages/scan/src/web/utils/parse-safe-area-option.ts rename to packages/scan/src/utils/parse-safe-area-option.ts index 7c70810f..d708d92a 100644 --- a/packages/scan/src/web/utils/parse-safe-area-option.ts +++ b/packages/scan/src/utils/parse-safe-area-option.ts @@ -1,14 +1,20 @@ -import type { Options } from '~core/index'; -import { isFiniteNonNegative } from '~web/utils/is-finite-non-negative'; -import { isPlainObject } from '~web/utils/is-plain-object'; +import { isFiniteNonNegative } from "./is-finite-non-negative"; +import { isPlainObject } from "./is-plain-object"; -type SafeAreaOption = NonNullable; +export interface SafeAreaInsets { + top?: number; + right?: number; + bottom?: number; + left?: number; +} + +export type SafeAreaOption = number | SafeAreaInsets; export type ParsedSafeAreaOption = | { ok: true; value: SafeAreaOption } | { ok: false; error: string }; -const SAFE_AREA_EDGES = ['top', 'right', 'bottom', 'left'] as const; +const SAFE_AREA_EDGES = ["top", "right", "bottom", "left"] as const; export const parseSafeAreaOption = (value: unknown): ParsedSafeAreaOption => { if (isFiniteNonNegative(value)) { @@ -22,7 +28,7 @@ export const parseSafeAreaOption = (value: unknown): ParsedSafeAreaOption => { }; } - const inset: Partial> = {}; + const inset: SafeAreaInsets = {}; for (const edge of SAFE_AREA_EDGES) { const edgeValue = value[edge]; if (edgeValue === undefined) continue; @@ -34,5 +40,6 @@ export const parseSafeAreaOption = (value: unknown): ParsedSafeAreaOption => { } inset[edge] = edgeValue; } + return { ok: true, value: inset }; }; diff --git a/packages/scan/src/utils/read-local-storage.ts b/packages/scan/src/utils/read-local-storage.ts new file mode 100644 index 00000000..ed011376 --- /dev/null +++ b/packages/scan/src/utils/read-local-storage.ts @@ -0,0 +1,12 @@ +import { IS_CLIENT } from "./is-client"; + +export const readLocalStorage = (storageKey: string): Value | null => { + if (!IS_CLIENT) return null; + + try { + const storedValue = localStorage.getItem(storageKey); + return storedValue ? JSON.parse(storedValue) : null; + } catch { + return null; + } +}; diff --git a/packages/scan/src/utils/save-local-storage.ts b/packages/scan/src/utils/save-local-storage.ts new file mode 100644 index 00000000..8fb7ac8e --- /dev/null +++ b/packages/scan/src/utils/save-local-storage.ts @@ -0,0 +1,9 @@ +import { IS_CLIENT } from "./is-client"; + +export const saveLocalStorage = (storageKey: string, state: Value): void => { + if (!IS_CLIENT) return; + + try { + window.localStorage.setItem(storageKey, JSON.stringify(state)); + } catch {} +}; diff --git a/packages/scan/src/web/assets/css/styles.tailwind.css b/packages/scan/src/web/assets/css/styles.tailwind.css index 445e8194..be2f5c3b 100644 --- a/packages/scan/src/web/assets/css/styles.tailwind.css +++ b/packages/scan/src/web/assets/css/styles.tailwind.css @@ -1,6 +1,99 @@ @import "tailwindcss"; @config "../../../../tailwind.config.mjs"; +:host, +:host([data-react-scan-theme="dark"]) { + --rs-panel-bg: #161616; + --rs-panel-raised: #202020; + --rs-text-primary: #fff; + --rs-text-secondary: #a7a7a7; + --rs-surface-hover: rgb(255 255 255 / 10%); + --rs-surface-active: rgb(255 255 255 / 15%); + --rs-border-subtle: rgb(255 255 255 / 10%); + --rs-shadow: 0 2px 8px rgb(0 0 0 / 8%); + --rs-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); + --rs-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); +} + +:host([data-react-scan-theme="light"]) { + --rs-panel-bg: #fff; + --rs-panel-raised: #f5f5f5; + --rs-text-primary: #171717; + --rs-text-secondary: #737373; + --rs-surface-hover: rgb(0 0 0 / 5%); + --rs-surface-active: rgb(0 0 0 / 8%); + --rs-border-subtle: rgb(0 0 0 / 8%); + --rs-shadow: 0 2px 12px rgb(0 0 0 / 10%); +} + +@keyframes react-scan-tooltip-in { + from { + opacity: 0; + transform: scale(0.97); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes react-scan-shake { + 0%, + 100% { + translate: 0; + } + 25% { + translate: -3px; + } + 50% { + translate: 3px; + } + 75% { + translate: -2px; + } +} + +.react-scan-interactive-scale { + transition: transform 400ms var(--rs-ease-spring); + + @media (hover: hover) and (pointer: fine) { + &:hover { + transform: scale(1.05); + } + } + + &:active { + transform: scale(0.96); + transition: transform 60ms cubic-bezier(0, 0, 0.2, 1); + } +} + +.react-scan-a11y-hitbox { + position: relative; + + &::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: max(100%, 24px); + height: max(100%, 24px); + transform: translate(-50%, -50%); + pointer-events: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + :host, + :host *, + :host *::before, + :host *::after { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +} + * { outline: none !important; text-rendering: optimizeLegibility; @@ -90,16 +183,6 @@ svg { #react-scan-toolbar { @apply fixed left-0 top-0; - @apply flex flex-col; - @apply shadow-lg; - @apply font-mono text-[13px] text-white; - @apply bg-black; - @apply select-none; - @apply cursor-move; - @apply opacity-0; - @apply z-[2147483678]; - @apply animate-fade-in animation-duration-300 animation-delay-300; - @apply shadow-[0_4px_12px_rgba(0,0,0,0.2)]; @apply place-self-start; will-change: transform; @@ -110,8 +193,8 @@ svg { * content elements (e.g. AI prompt
, search inputs). See #415. */
 #react-scan-toolbar pre,
 #react-scan-toolbar textarea,
-#react-scan-toolbar input[type='text'],
-#react-scan-toolbar input[type='search'],
+#react-scan-toolbar input[type="text"],
+#react-scan-toolbar input[type="search"],
 #react-scan-toolbar [data-react-scan-selectable] {
   @apply select-text;
   cursor: text;
@@ -156,33 +239,33 @@ svg {
 
   &:hover {
     .resize-line {
-      @apply translate-x-0;
+      transform: translateX(0);
     }
   }
 }
 .resize-right {
   @apply right-0;
-  @apply translate-x-1/2;
+  transform: translateX(50%);
 
   .resize-line-wrapper {
     @apply right-0;
   }
   .resize-line {
     @apply rounded-r-lg;
-    @apply -translate-x-full;
+    transform: translateX(-100%);
   }
 }
 
 .resize-left {
   @apply left-0;
-  @apply -translate-x-1/2;
+  transform: translateX(-50%);
 
   .resize-line-wrapper {
     @apply left-0;
   }
   .resize-line {
     @apply rounded-l-lg;
-    @apply translate-x-full;
+    transform: translateX(100%);
   }
 }
 
@@ -199,33 +282,33 @@ svg {
 
   &:hover {
     .resize-line {
-      @apply translate-y-0;
+      transform: translateY(0);
     }
   }
 }
 .resize-top {
   @apply top-0;
-  @apply -translate-y-1/2;
+  transform: translateY(-50%);
 
   .resize-line-wrapper {
     @apply top-0;
   }
   .resize-line {
     @apply rounded-t-lg;
-    @apply translate-y-full;
+    transform: translateY(100%);
   }
 }
 
 .resize-bottom {
   @apply bottom-0;
-  @apply translate-y-1/2;
+  transform: translateY(50%);
 
   .resize-line-wrapper {
     @apply bottom-0;
   }
   .resize-line {
     @apply rounded-b-lg;
-    @apply -translate-y-full;
+    transform: translateY(-100%);
   }
 }
 
@@ -266,12 +349,7 @@ svg {
     @apply inset-0;
     @apply -translate-x-full;
     animation: shimmer 2s infinite;
-    background: linear-gradient(
-      to right,
-      transparent,
-      rgba(142, 97, 227, 0.3),
-      transparent
-    );
+    background: linear-gradient(to right, transparent, rgba(142, 97, 227, 0.3), transparent);
   }
 }
 
@@ -425,9 +503,9 @@ svg {
       @apply bg-[#5f3f9a];
 
       &::before {
-        @apply translate-x-full;
         @apply left-auto;
         @apply border-[#5f3f9a];
+        transform: translate(100%, -50%);
       }
     }
   }
@@ -442,13 +520,13 @@ svg {
     &:before {
       @apply content-[''];
       @apply absolute top-1/2 left-0;
-      @apply -translate-y-1/2;
       @apply w-4 h-4;
       @apply bg-white;
       @apply border-2 border-neutral-700;
       @apply rounded-full;
       @apply shadow-sm;
       @apply transition-all duration-300;
+      transform: translateY(-50%);
     }
   }
 }
@@ -673,8 +751,7 @@ svg {
   }
 }
 
-.react-scan-components-tree:has(.resize-v-line:hover, .resize-v-line:active)
-  .tree {
+.react-scan-components-tree:has(.resize-v-line:hover, .resize-v-line:active) .tree {
   overflow: hidden;
 }
 
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 a9aa8287..f01ee109 100644
--- a/packages/scan/src/web/components/copy-to-clipboard/index.tsx
+++ b/packages/scan/src/web/components/copy-to-clipboard/index.tsx
@@ -1,86 +1,72 @@
-import type { JSX } from 'preact';
-import { memo } from 'preact/compat';
-import { useCallback, useEffect, useState } from 'preact/hooks';
-import { cn } from '~web/utils/helpers';
-import { Icon } from '../icon';
+import { createSignal, onCleanup, type JSX } from "solid-js";
+import { COPY_FEEDBACK_DURATION_MS } from "../../constants";
+import { cn } from "../../utils/helpers";
+import { Icon } from "../icon";
 
 interface CopyToClipboardProps {
   text: string;
   children?: (props: {
     ClipboardIcon: JSX.Element;
-    onClick: (e: MouseEvent) => void;
+    onClick: (event: MouseEvent) => void;
   }) => JSX.Element;
   onCopy?: (success: boolean, text: string) => void;
-  className?: string;
+  class?: string;
   iconSize?: number;
 }
 
-export const CopyToClipboard = /* @__PURE__ */ memo(
-  ({
-    text,
-    children,
-    onCopy,
-    className,
-    iconSize = 14,
-  }: CopyToClipboardProps): JSX.Element => {
-    const [isCopied, setIsCopied] = useState(false);
+export const CopyToClipboard = (props: CopyToClipboardProps): JSX.Element => {
+  const [isCopied, setIsCopied] = createSignal(false);
+  let resetTimeoutId: ReturnType | undefined;
 
-    useEffect(() => {
-      if (isCopied) {
-        const timeout = setTimeout(() => setIsCopied(false), 600);
-        return () => {
-          clearTimeout(timeout);
-        };
-      }
-    }, [isCopied]);
+  const copyToClipboard = (event: MouseEvent) => {
+    event.preventDefault();
+    event.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(props.text).then(
+      () => {
+        setIsCopied(true);
+        props.onCopy?.(true, props.text);
+        clearTimeout(resetTimeoutId);
+        resetTimeoutId = setTimeout(() => setIsCopied(false), COPY_FEEDBACK_DURATION_MS);
+      },
+      () => {
+        props.onCopy?.(false, props.text);
       },
-      [text, onCopy],
     );
+  };
+
+  onCleanup(() => {
+    clearTimeout(resetTimeoutId);
+  });
 
-    const ClipboardIcon = (
+  const clipboardIcon = () => {
+    const iconSize = props.iconSize ?? 14;
+    return (
       
     );
+  };
 
-    if (!children) {
-      return ClipboardIcon;
-    }
+  if (!props.children) {
+    return clipboardIcon();
+  }
 
-    return children({
-      ClipboardIcon,
-      onClick: copyToClipboard,
-    });
-  },
-);
+  return props.children({
+    ClipboardIcon: clipboardIcon(),
+    onClick: copyToClipboard,
+  });
+};
diff --git a/packages/scan/src/web/components/icon/index.tsx b/packages/scan/src/web/components/icon/index.tsx
index 39e1cc8c..cd0b2777 100644
--- a/packages/scan/src/web/components/icon/index.tsx
+++ b/packages/scan/src/web/components/icon/index.tsx
@@ -1,48 +1,45 @@
-import type { JSX } from 'preact';
-import { type ForwardedRef, forwardRef } from 'preact/compat';
+import type { JSX } from "solid-js";
 
 export interface SVGIconProps {
   size?: number | Array;
   name: string;
   fill?: string;
   stroke?: string;
-  className?: string;
+  class?: string;
   externalURL?: string;
   style?: JSX.CSSProperties;
 }
 
-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 path = `${externalURL}#${name}`;
+export const Icon = (props: SVGIconProps) => {
+  const dimensions = () => {
+    const size = props.size ?? 15;
+    if (Array.isArray(size)) {
+      return {
+        width: size[0],
+        height: size[1] || size[0],
+      };
+    }
+    return { width: size, height: size };
+  };
+  const path = () => `${props.externalURL ?? ""}#${props.name}`;
 
   return (
     
   );
-});
+};
diff --git a/packages/scan/src/web/components/svg-sprite/index.tsx b/packages/scan/src/web/components/svg-sprite/index.tsx
index 7a5a9c20..bbe51bb3 100644
--- a/packages/scan/src/web/components/svg-sprite/index.tsx
+++ b/packages/scan/src/web/components/svg-sprite/index.tsx
@@ -2,7 +2,36 @@ export const SvgSprite = () => {
   return (
     
       React Scan Icons
-      
+      
+      
         
         
         
@@ -15,98 +44,125 @@ export const SvgSprite = () => {
         
       
 
-      
+      
         
         
       
 
-      
-        
-      
-
-      
-        
-      
-
-      
+      
         
         
       
 
-      
-        
-        
-        
-        
-        
-        
-      
-
-      
+      
         
         
         
       
 
-      
+      
         
         
       
 
-      
+      
         
       
 
-      
+      
         
       
 
-      
-        
-      
-
       
         
       
 
-      
+      
         
         
         
       
 
-      
+      
         
         
         
       
 
-      
-        
-        
-      
-
-      
+      
         
         
       
-
-      
-        
-        
-      
-
-      
-        
-        
-      
-
-      
-        
-        
-        
-        
-      
     
-  )
+  );
 };
diff --git a/packages/scan/src/web/components/toggle/index.tsx b/packages/scan/src/web/components/toggle/index.tsx
index 2b6ae2b9..9bd71a10 100644
--- a/packages/scan/src/web/components/toggle/index.tsx
+++ b/packages/scan/src/web/components/toggle/index.tsx
@@ -1,22 +1,18 @@
-import type { JSX } from 'preact';
-import { cn } from '~web/utils/helpers';
+import { splitProps, type JSX } from "solid-js";
+import { cn } from "../../utils/helpers";
 
 interface ToggleProps extends JSX.HTMLAttributes {
   checked: boolean;
-  onChange: ((e: Event) => void);
-  className?: string;
-};
+  onChange: (event: Event) => void;
+  class?: string;
+}
+
+export const Toggle = (props: ToggleProps) => {
+  const [localProps, inputProps] = splitProps(props, ["class"]);
 
-export const Toggle = ({
-  className,
-  ...props
-}: ToggleProps) => {
   return (
-    
- +
+
); diff --git a/packages/scan/src/web/components/tooltip/index.tsx b/packages/scan/src/web/components/tooltip/index.tsx new file mode 100644 index 00000000..83f389ba --- /dev/null +++ b/packages/scan/src/web/components/tooltip/index.tsx @@ -0,0 +1,74 @@ +import { createEffect, createSignal, onCleanup, Show, type JSX } from "solid-js"; +import { TOOLBAR_Z_INDEX, TOOLTIP_DELAY_MS, TOOLTIP_GRACE_PERIOD_MS } from "../../constants"; +import { cn } from "../../utils/helpers"; + +interface TooltipProps { + visible: boolean; + position: "top" | "bottom" | "left" | "right"; + children: JSX.Element; +} + +let lastCloseTimestamp = 0; + +export const Tooltip = (props: TooltipProps) => { + const [isVisible, setIsVisible] = createSignal(false); + const [shouldAnimate, setShouldAnimate] = createSignal(true); + let delayTimeoutId: ReturnType | undefined; + + createEffect(() => { + clearTimeout(delayTimeoutId); + if (!props.visible) { + if (isVisible()) lastCloseTimestamp = Date.now(); + setIsVisible(false); + return; + } + + if (Date.now() - lastCloseTimestamp < TOOLTIP_GRACE_PERIOD_MS) { + setShouldAnimate(false); + setIsVisible(true); + return; + } + + setShouldAnimate(true); + delayTimeoutId = setTimeout(() => setIsVisible(true), TOOLTIP_DELAY_MS); + }); + + onCleanup(() => { + clearTimeout(delayTimeoutId); + if (isVisible()) lastCloseTimestamp = Date.now(); + }); + + const positionStyle = (): JSX.CSSProperties => + props.position === "top" || props.position === "bottom" + ? { + left: "50%", + translate: "-50%", + "z-index": TOOLBAR_Z_INDEX, + } + : { + top: "50%", + translate: "0 -50%", + "z-index": TOOLBAR_Z_INDEX, + }; + + return ( + +
+ {props.children} +
+
+ ); +}; diff --git a/packages/scan/src/web/constants.ts b/packages/scan/src/web/constants.ts index 52ec0b96..a7245ebe 100644 --- a/packages/scan/src/web/constants.ts +++ b/packages/scan/src/web/constants.ts @@ -1,5 +1,6 @@ export const SAFE_AREA = 24; export const COPY_FEEDBACK_DURATION_MS = 600; +export const FPS_SAMPLE_INTERVAL_MS = 200; export const MIN_SIZE = { width: 550, height: 350, @@ -9,11 +10,23 @@ export const MIN_SIZE = { export const MIN_CONTAINER_WIDTH = 240; export const LOCALSTORAGE_KEY = "react-scan-widget-settings-v2"; -export const LOCALSTORAGE_COLLAPSED_KEY = "react-scan-widget-collapsed-v1"; -export const LOCALSTORAGE_LAST_VIEW_KEY = "react-scan-widget-last-view-v1"; +export const LOCALSTORAGE_TOOLBAR_STATE_KEY = "react-scan-toolbar-state-v1"; -// CSS selector for elements inside #react-scan-toolbar that should NOT -// trigger drag and SHOULD allow native text selection / focus (#415). -// Keep in sync with the matching CSS rule in styles.tailwind.css. -export const TOOLBAR_INTERACTIVE_SELECTOR = - "button, a, input, textarea, select, pre, [contenteditable], [data-react-scan-selectable]"; +export const TOOLBAR_SNAP_MARGIN_PX = 16; +export const TOOLBAR_FADE_IN_DELAY_MS = 500; +export const TOOLBAR_SNAP_ANIMATION_DURATION_MS = 300; +export const TOOLBAR_DRAG_THRESHOLD_PX = 5; +export const TOOLBAR_VELOCITY_MULTIPLIER_MS = 150; +export const TOOLBAR_COLLAPSED_SHORT_PX = 16; +export const TOOLBAR_COLLAPSED_LONG_PX = 30; +export const TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS = 260; +export const TOOLBAR_DEFAULT_WIDTH_PX = 130; +export const TOOLBAR_DEFAULT_HEIGHT_PX = 28; +export const TOOLBAR_DEFAULT_POSITION_RATIO = 0.5; +export const TOOLTIP_DELAY_MS = 400; +export const TOOLTIP_GRACE_PERIOD_MS = 800; +export const THEME_DARK_LUMINANCE_THRESHOLD = 0.18; +export const RGB_CHANNEL_MAX = 255; +export const TOOLBAR_Z_INDEX = 2_147_483_678; +export const NOTIFICATION_LAG_MS = 600; +export const HEADER_TRANSITION_DELAY_MS = 150; diff --git a/packages/scan/src/web/hooks/use-delayed-value.ts b/packages/scan/src/web/hooks/use-delayed-value.ts index 1306a354..50d01fd7 100644 --- a/packages/scan/src/web/hooks/use-delayed-value.ts +++ b/packages/scan/src/web/hooks/use-delayed-value.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'preact/hooks'; +import { createEffect, createSignal, onCleanup, untrack, type Accessor } from "solid-js"; /** * Delays a boolean value change by a specified duration. @@ -32,26 +32,21 @@ import { useEffect, useState } from 'preact/hooks'; *
* ); */ -export const useDelayedValue = ( - value: boolean, +export const createDelayedValue = ( + value: Accessor, onDelay: number, - offDelay: number = onDelay, -): boolean => { - const [delayedValue, setDelayedValue] = useState(value); + offDelay = onDelay, +): Accessor => { + const [delayedValue, setDelayedValue] = createSignal(value()); - /* - * oxlint-disable-next-line react-hooks/exhaustive-deps - * delayedValue is intentionally omitted to prevent unnecessary timeouts - * and used only in the early return check - */ - useEffect(() => { - if (value === delayedValue) return; + createEffect(() => { + const nextValue = value(); + if (nextValue === untrack(delayedValue)) return; - const delay = value ? onDelay : offDelay; - const timeout = setTimeout(() => setDelayedValue(value), delay); - - return () => clearTimeout(timeout); - }, [value, onDelay, offDelay]); + const delay = nextValue ? onDelay : offDelay; + const timeoutId = setTimeout(() => setDelayedValue(nextValue), delay); + onCleanup(() => clearTimeout(timeoutId)); + }); return delayedValue; }; diff --git a/packages/scan/src/web/hooks/use-virtual-list.ts b/packages/scan/src/web/hooks/use-virtual-list.ts deleted file mode 100644 index 18b2cdb2..00000000 --- a/packages/scan/src/web/hooks/use-virtual-list.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'preact/hooks'; - -export interface VirtualItem { - key: number; - index: number; - start: number; -} - -export const useVirtualList = (options: { - count: number; - getScrollElement: () => HTMLElement | null; - estimateSize: () => number; - overscan?: number; -}) => { - const { count, getScrollElement, estimateSize, overscan = 5 } = options; - const [scrollTop, setScrollTop] = useState(0); - const [containerHeight, setContainerHeight] = useState(0); - const refResizeObserver = useRef(); - const refScrollElement = useRef(null); - const refRafId = useRef(null); - const itemHeight = estimateSize(); - - const updateContainer = useCallback((entries?: ResizeObserverEntry[]) => { - if (!refScrollElement.current) return; - - const height = - entries?.[0]?.contentRect.height ?? - refScrollElement.current.getBoundingClientRect().height; - setContainerHeight(height); - }, []); - - const debouncedUpdateContainer = useCallback(() => { - if (refRafId.current !== null) { - cancelAnimationFrame(refRafId.current); - } - refRafId.current = requestAnimationFrame(() => { - updateContainer(); - refRafId.current = null; - }); - }, [updateContainer]); - - useEffect(() => { - const element = getScrollElement(); - if (!element) return; - - refScrollElement.current = element; - - const handleScroll = () => { - if (!refScrollElement.current) return; - setScrollTop(refScrollElement.current.scrollTop); - }; - - updateContainer(); - - if (!refResizeObserver.current) { - refResizeObserver.current = new ResizeObserver(() => { - debouncedUpdateContainer(); - }); - } - refResizeObserver.current.observe(element); - - element.addEventListener('scroll', handleScroll, { passive: true }); - - const mutationObserver = new MutationObserver(debouncedUpdateContainer); - mutationObserver.observe(element, { - attributes: true, - childList: true, - subtree: true, - }); - - return () => { - element.removeEventListener('scroll', handleScroll); - if (refResizeObserver.current) { - refResizeObserver.current.disconnect(); - } - mutationObserver.disconnect(); - if (refRafId.current !== null) { - cancelAnimationFrame(refRafId.current); - } - }; - }, [getScrollElement, updateContainer, debouncedUpdateContainer]); - - const visibleRange = useMemo(() => { - const start = Math.floor(scrollTop / itemHeight); - const visibleCount = Math.ceil(containerHeight / itemHeight); - - return { - start: Math.max(0, start - overscan), - end: Math.min(count, start + visibleCount + overscan), - }; - }, [scrollTop, itemHeight, containerHeight, count, overscan]); - - const items = useMemo(() => { - const virtualItems: VirtualItem[] = []; - for (let index = visibleRange.start; index < visibleRange.end; index++) { - virtualItems.push({ - key: index, - index, - start: index * itemHeight, - }); - } - return virtualItems; - }, [visibleRange, itemHeight]); - - return { - virtualItems: items, - totalSize: count * itemHeight, - scrollTop, - containerHeight, - }; -}; diff --git a/packages/scan/src/web/state.ts b/packages/scan/src/web/state.ts index b09792e0..f2db8727 100644 --- a/packages/scan/src/web/state.ts +++ b/packages/scan/src/web/state.ts @@ -1,18 +1,16 @@ -import { signal } from "@preact/signals"; +import { createSignal } from "solid-js"; import { LOCALSTORAGE_KEY, - LOCALSTORAGE_COLLAPSED_KEY, + LOCALSTORAGE_TOOLBAR_STATE_KEY, MIN_CONTAINER_WIDTH, MIN_SIZE, SAFE_AREA, + TOOLBAR_DEFAULT_POSITION_RATIO, } from "./constants"; -import { IS_CLIENT } from "./utils/constants"; import { readLocalStorage, saveLocalStorage } from "./utils/helpers"; -import { getSafeArea } from "./utils/safe-area"; -import type { CollapsedPosition, Corner, WidgetConfig, WidgetSettings } from "./widget/types"; +import type { Corner, SnapEdge, ToolbarState, WidgetConfig, WidgetSettings } from "./widget/types"; -export const signalIsSettingsOpen = /* @__PURE__ */ signal(false); -export const signalRefWidget = /* @__PURE__ */ signal(null); +export const [getWidgetRef, setWidgetRef] = createSignal(null); // Use the raw SAFE_AREA constant (not getSafeArea()) here: this runs at // module-init time, before any user has called scan() with options. @@ -37,11 +35,6 @@ export const getDefaultWidgetConfig = (): WidgetConfig => ({ }, }); -// Deprecated alias kept for one minor version to avoid breaking downstream -// imports of the pre-refactor `defaultWidgetConfig` const. -/** @deprecated use {@link getDefaultWidgetConfig} */ -export const defaultWidgetConfig: WidgetConfig = getDefaultWidgetConfig(); - const getInitialWidgetConfig = (): WidgetConfig => { const defaults = getDefaultWidgetConfig(); const stored = readLocalStorage(LOCALSTORAGE_KEY); @@ -65,26 +58,8 @@ const getInitialWidgetConfig = (): WidgetConfig => { }; }; -export const signalWidget = signal(getInitialWidgetConfig()); - -export const updateDimensions = (): void => { - if (!IS_CLIENT) return; - - const { dimensions } = signalWidget.value; - const { width, height, position } = dimensions; - const safeArea = getSafeArea(); - - signalWidget.value = { - ...signalWidget.value, - dimensions: { - isFullWidth: width >= window.innerWidth - safeArea.left - safeArea.right, - isFullHeight: height >= window.innerHeight - safeArea.top - safeArea.bottom, - width, - height, - position, - }, - }; -}; +export const [getWidgetState, setWidgetState] = + createSignal(getInitialWidgetConfig()); export type WidgetStates = | { @@ -106,10 +81,27 @@ export type WidgetStates = // view: 'summary'; // // extra params // }; -export const signalWidgetViews = signal({ +export const [getWidgetView, setWidgetView] = createSignal({ view: "none", }); -const storedCollapsed = readLocalStorage(LOCALSTORAGE_COLLAPSED_KEY); -export const signalWidgetCollapsed = - /* @__PURE__ */ signal(storedCollapsed ?? null); +const isSnapEdge = (edge: unknown): edge is SnapEdge => + edge === "top" || edge === "bottom" || edge === "left" || edge === "right"; + +const getInitialToolbarState = (): ToolbarState => { + const stored = readLocalStorage>(LOCALSTORAGE_TOOLBAR_STATE_KEY); + return { + edge: isSnapEdge(stored?.edge) ? stored.edge : "bottom", + ratio: typeof stored?.ratio === "number" ? stored.ratio : TOOLBAR_DEFAULT_POSITION_RATIO, + collapsed: stored?.collapsed === true, + }; +}; + +export const [getToolbarState, setToolbarState] = + createSignal(getInitialToolbarState()); + +export const updateToolbarState = (update: (state: ToolbarState) => ToolbarState): void => { + const nextState = update(getToolbarState()); + setToolbarState(nextState); + saveLocalStorage(LOCALSTORAGE_TOOLBAR_STATE_KEY, nextState); +}; diff --git a/packages/scan/src/web/toolbar.tsx b/packages/scan/src/web/toolbar.tsx index 8fb97e08..71d3600e 100644 --- a/packages/scan/src/web/toolbar.tsx +++ b/packages/scan/src/web/toolbar.tsx @@ -1,79 +1,61 @@ -import { Component, render } from 'preact'; -import { Icon } from './components/icon'; -import { Widget } from './widget'; -import { SvgSprite } from './components/svg-sprite'; - - -class ToolbarErrorBoundary extends Component { - state: { hasError: boolean; error: Error | null } = { hasError: false, error: null }; - - static getDerivedStateFromError(error: Error) { - return { hasError: true, error }; - } - - handleReset = () => { - this.setState({ hasError: false, error: null }); - }; - - render() { - if (this.state.hasError) { - return ( -
-
-
- - React Scan ran into a problem -
-
- {this.state.error?.message || JSON.stringify(this.state.error)} -
- -
-
- ); - } - - return this.props.children; - } +import { ErrorBoundary } from "solid-js"; +import { render } from "solid-js/web"; +import { Icon } from "./components/icon"; +import { SvgSprite } from "./components/svg-sprite"; +import { Widget } from "./widget"; + +export interface ToolbarController { + container: HTMLDivElement; + dispose: () => void; } -export const createToolbar = (root: ShadowRoot): HTMLElement => { - const container = document.createElement('div'); - container.id = 'react-scan-toolbar-root'; +const ToolbarErrorFallback = (error: Error, reset: () => void) => ( +
+
+
+ + React Scan ran into a problem +
+
+ {error.message || JSON.stringify(error)} +
+ +
+
+); + +export const createToolbar = (root: ShadowRoot): ToolbarController => { + const container = document.createElement("div"); + container.id = "react-scan-toolbar-root"; window.__REACT_SCAN_TOOLBAR_CONTAINER__ = container; root.appendChild(container); - render( - - <> - - - - , + const dispose = render( + () => ( + + <> + + + + + ), container, ); - const originalRemove = container.remove.bind(container); - - container.remove = () => { - window.__REACT_SCAN_TOOLBAR_CONTAINER__ = undefined; - - if (container.hasChildNodes()) { - // Double render(null) is needed to fully unmount Preact components. - // The first call initiates unmounting, while the second ensures - // cleanup of internal VNode references and event listeners. - render(null, container); - render(null, container); - } - - originalRemove(); + return { + container, + dispose: () => { + if (window.__REACT_SCAN_TOOLBAR_CONTAINER__ === container) { + window.__REACT_SCAN_TOOLBAR_CONTAINER__ = undefined; + } + dispose(); + container.remove(); + }, }; - - return container; }; diff --git a/packages/scan/src/web/utils/clamp-to-range.ts b/packages/scan/src/web/utils/clamp-to-range.ts new file mode 100644 index 00000000..62535888 --- /dev/null +++ b/packages/scan/src/web/utils/clamp-to-range.ts @@ -0,0 +1,2 @@ +export const clampToRange = (value: number, minimum: number, maximum: number): number => + Math.min(Math.max(value, minimum), Math.max(minimum, maximum)); diff --git a/packages/scan/src/web/utils/constants.ts b/packages/scan/src/web/utils/constants.ts deleted file mode 100644 index c8e4e4c8..00000000 --- a/packages/scan/src/web/utils/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const IS_CLIENT = typeof window !== 'undefined'; diff --git a/packages/scan/src/web/utils/create-store.ts b/packages/scan/src/web/utils/create-store.ts deleted file mode 100644 index 759f705d..00000000 --- a/packages/scan/src/web/utils/create-store.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Adapted from zustand v5.0.3 - * - * https://github.com/pmndrs/zustand - * - * Do not modify unless you know what you are doing - */ -type SetStateInternal = { - _(partial: T | Partial | { _(state: T): T | Partial }["_"], replace?: false): void; - _(state: T | { _(state: T): T }["_"], replace: true): void; -}["_"]; - -export interface StoreApi { - setState: SetStateInternal; - getState: () => T; - getInitialState: () => T; - subscribe: { - (listener: (state: T, prevState: T) => void): () => void; - ( - selector: (state: T) => U, - listener: (selectedState: U, prevSelectedState: U) => void, - ): () => void; - }; -} - -type Get = K extends keyof T ? T[K] : F; - -export type Mutate = number extends Ms["length" & keyof Ms] - ? S - : Ms extends [] - ? S - : Ms extends [[infer Mi, infer Ma], ...infer Mrs] - ? Mutate[Mi & StoreMutatorIdentifier], Mrs> - : never; - -export type StateCreator< - T, - Mis extends [StoreMutatorIdentifier, unknown][] = [], - Mos extends [StoreMutatorIdentifier, unknown][] = [], - U = T, -> = (( - setState: Get, Mis>, "setState", never>, - getState: Get, Mis>, "getState", never>, - store: Mutate, Mis>, -) => U) & { $$storeMutators?: Mos }; - -// oxlint-disable-next-line no-unused-vars -export interface StoreMutators {} -export type StoreMutatorIdentifier = keyof StoreMutators; - -type CreateStore = { - ( - initializer: StateCreator, - ): Mutate, Mos>; - - (): ( - initializer: StateCreator, - ) => Mutate, Mos>; -}; - -type CreateStoreImpl = ( - initializer: StateCreator, -) => Mutate, Mos>; - -const createStoreImpl: CreateStoreImpl = (createState) => { - type TState = ReturnType; - type Listener = (state: TState, prevState: TState) => void; - let state: TState; - const listeners: Set = new Set(); - - const setState: StoreApi["setState"] = (partial, replace) => { - const nextState = - typeof partial === "function" ? (partial as (state: TState) => TState)(state) : partial; - if (!Object.is(nextState, state)) { - const previousState = state; - state = - (replace ?? (typeof nextState !== "object" || nextState === null)) - ? (nextState as TState) - : Object.assign({}, state, nextState); - listeners.forEach((listener) => listener(state, previousState)); - } - }; - - const getState: StoreApi["getState"] = () => state; - - const getInitialState: StoreApi["getInitialState"] = () => initialState; - - const subscribe: StoreApi["subscribe"] = ( - selectorOrListener: - | ((state: TState, prevState: TState) => void) - // oxlint-disable-next-line typescript/no-explicit-any - | ((state: TState) => any), - // oxlint-disable-next-line typescript/no-explicit-any - listener?: (selectedState: any, prevSelectedState: any) => void, - ) => { - // oxlint-disable-next-line typescript/no-explicit-any - let selector: ((state: TState) => any) | undefined; - // oxlint-disable-next-line typescript/no-explicit-any - let actualListener: (state: any, prevState: any) => void; - - if (listener) { - // Selector subscription case - // oxlint-disable-next-line typescript/no-explicit-any - selector = selectorOrListener as (state: TState) => any; - actualListener = listener; - } else { - // Regular subscription case - actualListener = selectorOrListener as (state: TState, prevState: TState) => void; - } - - let currentSlice = selector ? selector(state) : undefined; - - const wrappedListener = (newState: TState, previousState: TState) => { - if (selector) { - const nextSlice = selector(newState); - const prevSlice = selector(previousState); - if (!Object.is(currentSlice, nextSlice)) { - currentSlice = nextSlice; - actualListener(nextSlice, prevSlice); - } - } else { - actualListener(newState, previousState); - } - }; - - listeners.add(wrappedListener); - // Unsubscribe - return () => listeners.delete(wrappedListener); - }; - - const api = { setState, getState, getInitialState, subscribe }; - const initialState = (state = createState(setState, getState, api)); - // oxlint-disable-next-line typescript/no-explicit-any - return api as any; -}; - -export const createStore = ((createState) => - createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore; diff --git a/packages/scan/src/web/utils/create-toolbar-drag.ts b/packages/scan/src/web/utils/create-toolbar-drag.ts new file mode 100644 index 00000000..bfc9a37b --- /dev/null +++ b/packages/scan/src/web/utils/create-toolbar-drag.ts @@ -0,0 +1,161 @@ +import { createSignal, onCleanup, type Accessor } from "solid-js"; +import { TOOLBAR_DRAG_THRESHOLD_PX, TOOLBAR_SNAP_ANIMATION_DURATION_MS } from "../constants"; +import type { Position, SnapEdge } from "../widget/types"; +import { + getPositionFromEdgeAndRatio, + getRatioFromPosition, + getSnapPosition, +} from "./toolbar-position"; + +interface ToolbarDragConfig { + getContainer: () => HTMLDivElement | undefined; + isCollapsed: Accessor; + onDragStart: () => void; + onPositionUpdate: (position: Position) => void; + onSnapEdgeChange: (edge: SnapEdge, ratio: number) => void; + onSnapComplete: (edge: SnapEdge, ratio: number, position: Position) => void; +} + +interface ToolbarDrag { + isDragging: Accessor; + isSnapping: Accessor; + handlePointerDown: (event: PointerEvent) => void; + createDragAwareHandler: (callback: () => void) => (event: MouseEvent) => void; +} + +export const createToolbarDrag = (config: ToolbarDragConfig): ToolbarDrag => { + const [isDragging, setIsDragging] = createSignal(false); + const [isSnapping, setIsSnapping] = createSignal(false); + let didMove = false; + let shouldSuppressClick = false; + let dragOffsetX = 0; + let dragOffsetY = 0; + let pointerStartX = 0; + let pointerStartY = 0; + let lastPointerX = 0; + let lastPointerY = 0; + let lastPointerTime = 0; + let velocityX = 0; + let velocityY = 0; + let dragAbortController: AbortController | undefined; + let snapFrameId: number | undefined; + let snapTimeoutId: ReturnType | undefined; + + const teardownDrag = () => { + dragAbortController?.abort(); + dragAbortController = undefined; + }; + + const handlePointerMove = (event: PointerEvent) => { + if (!didMove) { + const distance = Math.hypot(event.clientX - pointerStartX, event.clientY - pointerStartY); + if (distance <= TOOLBAR_DRAG_THRESHOLD_PX) return; + didMove = true; + config.onDragStart(); + } + + const currentTime = performance.now(); + const elapsedTime = currentTime - lastPointerTime; + if (elapsedTime > 0) { + velocityX = (event.clientX - lastPointerX) / elapsedTime; + velocityY = (event.clientY - lastPointerY) / elapsedTime; + } + lastPointerX = event.clientX; + lastPointerY = event.clientY; + lastPointerTime = currentTime; + config.onPositionUpdate({ + x: event.clientX - dragOffsetX, + y: event.clientY - dragOffsetY, + }); + }; + + const handlePointerEnd = () => { + teardownDrag(); + setIsDragging(false); + if (!didMove) return; + + shouldSuppressClick = true; + const container = config.getContainer(); + const currentBounds = container?.getBoundingClientRect(); + if (!currentBounds) return; + const snap = getSnapPosition( + currentBounds.left, + currentBounds.top, + currentBounds.width, + currentBounds.height, + velocityX, + velocityY, + ); + const ratio = getRatioFromPosition( + snap.edge, + snap.x, + snap.y, + currentBounds.width, + currentBounds.height, + ); + config.onSnapEdgeChange(snap.edge, ratio); + setIsSnapping(true); + + cancelAnimationFrame(snapFrameId ?? 0); + snapFrameId = requestAnimationFrame(() => { + const nextBounds = container?.getBoundingClientRect() ?? currentBounds; + snapFrameId = requestAnimationFrame(() => { + const position = getPositionFromEdgeAndRatio( + snap.edge, + ratio, + nextBounds.width, + nextBounds.height, + ); + config.onSnapComplete(snap.edge, ratio, position); + clearTimeout(snapTimeoutId); + snapTimeoutId = setTimeout(() => setIsSnapping(false), TOOLBAR_SNAP_ANIMATION_DURATION_MS); + }); + }); + }; + + const handlePointerDown = (event: PointerEvent) => { + if (event.button !== 0 || config.isCollapsed() || isSnapping()) return; + const bounds = config.getContainer()?.getBoundingClientRect(); + if (!bounds) return; + + pointerStartX = event.clientX; + pointerStartY = event.clientY; + lastPointerX = event.clientX; + lastPointerY = event.clientY; + lastPointerTime = performance.now(); + dragOffsetX = event.clientX - bounds.left; + dragOffsetY = event.clientY - bounds.top; + velocityX = 0; + velocityY = 0; + didMove = false; + setIsDragging(true); + teardownDrag(); + dragAbortController = new AbortController(); + const listenerOptions = { signal: dragAbortController.signal }; + window.addEventListener("pointermove", handlePointerMove, listenerOptions); + window.addEventListener("pointerup", handlePointerEnd, listenerOptions); + window.addEventListener("pointercancel", handlePointerEnd, listenerOptions); + }; + + const createDragAwareHandler = (callback: () => void) => (event: MouseEvent) => { + event.stopImmediatePropagation(); + if (shouldSuppressClick) { + shouldSuppressClick = false; + return; + } + callback(); + }; + + onCleanup(() => { + teardownDrag(); + cancelAnimationFrame(snapFrameId ?? 0); + clearTimeout(snapTimeoutId); + }); + + return { + isDragging, + isSnapping, + handlePointerDown, + createDragAwareHandler, + }; +}; diff --git a/packages/scan/src/web/utils/get-corner-from-edge.ts b/packages/scan/src/web/utils/get-corner-from-edge.ts new file mode 100644 index 00000000..2136808c --- /dev/null +++ b/packages/scan/src/web/utils/get-corner-from-edge.ts @@ -0,0 +1,11 @@ +import type { Corner, SnapEdge } from "../widget/types"; + +export const getCornerFromEdge = (edge: SnapEdge, ratio: number): Corner => { + const isNearStart = ratio < 0.5; + if (edge === "top") return isNearStart ? "top-left" : "top-right"; + if (edge === "bottom") { + return isNearStart ? "bottom-left" : "bottom-right"; + } + if (edge === "left") return isNearStart ? "top-left" : "bottom-left"; + return isNearStart ? "top-right" : "bottom-right"; +}; diff --git a/packages/scan/src/web/utils/get-visual-viewport.ts b/packages/scan/src/web/utils/get-visual-viewport.ts new file mode 100644 index 00000000..34eaf0cd --- /dev/null +++ b/packages/scan/src/web/utils/get-visual-viewport.ts @@ -0,0 +1,25 @@ +interface VisualViewportInfo { + width: number; + height: number; + offsetLeft: number; + offsetTop: number; +} + +export const getVisualViewport = (): VisualViewportInfo => { + const visualViewport = window.visualViewport; + if (visualViewport) { + return { + width: visualViewport.width, + height: visualViewport.height, + offsetLeft: visualViewport.offsetLeft, + offsetTop: visualViewport.offsetTop, + }; + } + + return { + width: window.innerWidth, + height: window.innerHeight, + offsetLeft: 0, + offsetTop: 0, + }; +}; diff --git a/packages/scan/src/web/utils/helpers.ts b/packages/scan/src/web/utils/helpers.ts index 2eb44f35..f8d978a1 100644 --- a/packages/scan/src/web/utils/helpers.ts +++ b/packages/scan/src/web/utils/helpers.ts @@ -7,9 +7,11 @@ import { hasMemoCache, } from "bippy"; import { type ClassValue, clsx } from "clsx"; -import { IS_CLIENT } from "./constants"; import { twMerge } from "tailwind-merge"; +export { readLocalStorage } from "../../utils/read-local-storage"; +export { saveLocalStorage } from "../../utils/save-local-storage"; + export const cn = (...inputs: Array): string => { return twMerge(clsx(inputs)); }; @@ -26,32 +28,6 @@ export const throttle = (callback: (e?: E) => void, delay: number): ((e?: E) }; }; -export const readLocalStorage = (storageKey: string): T | null => { - if (!IS_CLIENT) return null; - - try { - const stored = localStorage.getItem(storageKey); - return stored ? JSON.parse(stored) : null; - } catch { - return null; - } -}; - -export const saveLocalStorage = (storageKey: string, state: T): void => { - if (!IS_CLIENT) return; - - try { - window.localStorage.setItem(storageKey, JSON.stringify(state)); - } catch {} -}; -export const removeLocalStorage = (storageKey: string): void => { - if (!IS_CLIENT) return; - - try { - window.localStorage.removeItem(storageKey); - } catch {} -}; - interface WrapperBadge { type: "memo" | "forwardRef" | "lazy" | "suspense" | "profiler" | "strict"; title: string; diff --git a/packages/scan/src/web/utils/is-plain-object.ts b/packages/scan/src/web/utils/is-plain-object.ts deleted file mode 100644 index 2558f466..00000000 --- a/packages/scan/src/web/utils/is-plain-object.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const isPlainObject = ( - value: unknown, -): value is Record => - Boolean(value) && typeof value === 'object' && !Array.isArray(value); diff --git a/packages/scan/src/web/utils/log.ts b/packages/scan/src/web/utils/log.ts index 5ce0e27b..e081baa7 100644 --- a/packages/scan/src/web/utils/log.ts +++ b/packages/scan/src/web/utils/log.ts @@ -1,6 +1,5 @@ -// @ts-nocheck -import { ChangeReason, type Render } from "~core/instrumentation"; -import { getLabelText } from "~core/utils"; +import { ChangeReason, type Render, isValueUnstable } from "../../core/instrumentation"; +import { getLabelText } from "../../core/utils"; export const log = (renders: Array) => { const logMap = new Map< @@ -24,7 +23,9 @@ export const log = (renders: Array) => { changes: { // TODO(Alexis): use a faster reduction method type: render.changes.reduce((set, change) => set | change.type, 0), - unstable: render.changes.some((change) => change.unstable), + unstable: render.changes.some((change) => + isValueUnstable(change.prevValue, change.value), + ), }, phase: render.phase, computedCurrent: null, @@ -37,7 +38,8 @@ export const log = (renders: Array) => { if (render.changes) { for (let i = 0, len = render.changes.length; i < len; i++) { - const { name, prevValue, nextValue, unstable, type } = render.changes[i]; + const { name, prevValue, value: nextValue, type } = render.changes[i]; + const unstable = isValueUnstable(prevValue, nextValue); if (type === ChangeReason.Props) { prevChangedProps ??= {}; nextChangedProps ??= {}; diff --git a/packages/scan/src/web/utils/pin.ts b/packages/scan/src/web/utils/pin.ts index 8e10d77f..9057db44 100644 --- a/packages/scan/src/web/utils/pin.ts +++ b/packages/scan/src/web/utils/pin.ts @@ -1,4 +1,4 @@ -import type { Fiber } from 'bippy'; +import type { Fiber } from "bippy"; export const getFiberPath = (fiber: Fiber): string => { const pathSegments: string[] = []; @@ -7,18 +7,17 @@ export const getFiberPath = (fiber: Fiber): string => { while (currentFiber) { const elementType = currentFiber.elementType; const name = - typeof elementType === 'function' + typeof elementType === "function" ? elementType.displayName || elementType.name - : typeof elementType === 'string' + : typeof elementType === "string" ? elementType - : 'Unknown'; + : "Unknown"; - const index = - currentFiber.index !== undefined ? `[${currentFiber.index}]` : ''; + const index = currentFiber.index !== undefined ? `[${currentFiber.index}]` : ""; pathSegments.unshift(`${name}${index}`); currentFiber = currentFiber.return ?? null; } - return pathSegments.join('::'); + return pathSegments.join("::"); }; diff --git a/packages/scan/src/web/utils/preact/constant.ts b/packages/scan/src/web/utils/preact/constant.ts deleted file mode 100644 index 2b8ec617..00000000 --- a/packages/scan/src/web/utils/preact/constant.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - type Attributes, - type Component, - type FunctionComponent, - createElement, -} from 'preact'; - -function CONSTANT_UPDATE() { - return false; -} - -export function constant

( - Component: FunctionComponent

, -) { - function Memoed(this: Component

, props: P) { - this.shouldComponentUpdate = CONSTANT_UPDATE; - return createElement

(Component, props); - } - Memoed.displayName = `Memo(${Component.displayName || Component.name})`; - Memoed.prototype.isReactComponent = true; - Memoed._forwarded = true; - return Memoed; -} diff --git a/packages/scan/src/web/utils/safe-area.ts b/packages/scan/src/web/utils/safe-area.ts index 7a33ae9f..5a51a022 100644 --- a/packages/scan/src/web/utils/safe-area.ts +++ b/packages/scan/src/web/utils/safe-area.ts @@ -1,17 +1,17 @@ -import { ReactScanInternals } from '~core/index'; -import { SAFE_AREA } from '~web/constants'; -import { isFiniteNonNegative } from '~web/utils/is-finite-non-negative'; -import { isPlainObject } from '~web/utils/is-plain-object'; +import { getOptionsState } from "../../core/native-state"; +import { isFiniteNonNegative } from "../../utils/is-finite-non-negative"; +import { isPlainObject } from "../../utils/is-plain-object"; +import { SAFE_AREA } from "../constants"; -export interface SafeAreaInsets { +export interface ResolvedSafeAreaInsets { top: number; right: number; bottom: number; left: number; } -export const getSafeArea = (): SafeAreaInsets => { - const value = ReactScanInternals.options.value.safeArea; +export const getSafeArea = (): ResolvedSafeAreaInsets => { + const value = getOptionsState().safeArea; if (isFiniteNonNegative(value)) { return { top: value, right: value, bottom: value, left: value }; diff --git a/packages/scan/src/web/utils/toolbar-position.ts b/packages/scan/src/web/utils/toolbar-position.ts new file mode 100644 index 00000000..13205fec --- /dev/null +++ b/packages/scan/src/web/utils/toolbar-position.ts @@ -0,0 +1,177 @@ +import { + TOOLBAR_COLLAPSED_LONG_PX, + TOOLBAR_COLLAPSED_SHORT_PX, + TOOLBAR_DEFAULT_POSITION_RATIO, + TOOLBAR_SNAP_MARGIN_PX, + TOOLBAR_VELOCITY_MULTIPLIER_MS, +} from "../constants"; +import type { Position, SnapEdge } from "../widget/types"; +import { clampToRange } from "./clamp-to-range"; +import { getVisualViewport } from "./get-visual-viewport"; +import { getSafeArea } from "./safe-area"; + +interface Dimensions { + width: number; + height: number; +} + +interface SnapResult extends Position { + edge: SnapEdge; +} + +interface ViewportBounds { + minimumX: number; + maximumX: number; + minimumY: number; + maximumY: number; +} + +export const isHorizontalEdge = (edge: SnapEdge): boolean => edge === "top" || edge === "bottom"; + +export const getCollapsedDimensions = (edge: SnapEdge): Dimensions => { + const isHorizontal = isHorizontalEdge(edge); + return { + width: isHorizontal ? TOOLBAR_COLLAPSED_LONG_PX : TOOLBAR_COLLAPSED_SHORT_PX, + height: isHorizontal ? TOOLBAR_COLLAPSED_SHORT_PX : TOOLBAR_COLLAPSED_LONG_PX, + }; +}; + +const getViewportBounds = (elementWidth: number, elementHeight: number): ViewportBounds => { + const viewport = getVisualViewport(); + const safeArea = getSafeArea(); + const leftInset = Math.max(TOOLBAR_SNAP_MARGIN_PX, safeArea.left); + const rightInset = Math.max(TOOLBAR_SNAP_MARGIN_PX, safeArea.right); + const topInset = Math.max(TOOLBAR_SNAP_MARGIN_PX, safeArea.top); + const bottomInset = Math.max(TOOLBAR_SNAP_MARGIN_PX, safeArea.bottom); + const minimumX = viewport.offsetLeft + leftInset; + const minimumY = viewport.offsetTop + topInset; + + return { + minimumX, + maximumX: Math.max(minimumX, viewport.offsetLeft + viewport.width - elementWidth - rightInset), + minimumY, + maximumY: Math.max( + minimumY, + viewport.offsetTop + viewport.height - elementHeight - bottomInset, + ), + }; +}; + +export const getPositionFromEdgeAndRatio = ( + edge: SnapEdge, + ratio: number, + elementWidth: number, + elementHeight: number, +): Position => { + const bounds = getViewportBounds(elementWidth, elementHeight); + if (isHorizontalEdge(edge)) { + return { + x: bounds.minimumX + (bounds.maximumX - bounds.minimumX) * clampToRange(ratio, 0, 1), + y: edge === "top" ? bounds.minimumY : bounds.maximumY, + }; + } + + return { + x: edge === "left" ? bounds.minimumX : bounds.maximumX, + y: bounds.minimumY + (bounds.maximumY - bounds.minimumY) * clampToRange(ratio, 0, 1), + }; +}; + +export const getRatioFromPosition = ( + edge: SnapEdge, + positionX: number, + positionY: number, + elementWidth: number, + elementHeight: number, +): number => { + const bounds = getViewportBounds(elementWidth, elementHeight); + if (isHorizontalEdge(edge)) { + const availableWidth = bounds.maximumX - bounds.minimumX; + if (availableWidth <= 0) return TOOLBAR_DEFAULT_POSITION_RATIO; + return clampToRange((positionX - bounds.minimumX) / availableWidth, 0, 1); + } + + const availableHeight = bounds.maximumY - bounds.minimumY; + if (availableHeight <= 0) return TOOLBAR_DEFAULT_POSITION_RATIO; + return clampToRange((positionY - bounds.minimumY) / availableHeight, 0, 1); +}; + +export const getCollapsedPosition = ( + edge: SnapEdge, + expandedPosition: Position, + expandedDimensions: Dimensions, + collapsedDimensions: Dimensions, +): Position => { + const viewport = getVisualViewport(); + if (isHorizontalEdge(edge)) { + return { + x: clampToRange( + expandedPosition.x + (expandedDimensions.width - collapsedDimensions.width) / 2, + viewport.offsetLeft, + viewport.offsetLeft + viewport.width - collapsedDimensions.width, + ), + y: + edge === "top" + ? viewport.offsetTop + : viewport.offsetTop + viewport.height - collapsedDimensions.height, + }; + } + + return { + x: + edge === "left" + ? viewport.offsetLeft + : viewport.offsetLeft + viewport.width - collapsedDimensions.width, + y: clampToRange( + expandedPosition.y + (expandedDimensions.height - collapsedDimensions.height) / 2, + viewport.offsetTop, + viewport.offsetTop + viewport.height - collapsedDimensions.height, + ), + }; +}; + +export const getSnapPosition = ( + currentX: number, + currentY: number, + elementWidth: number, + elementHeight: number, + velocityX: number, + velocityY: number, +): SnapResult => { + const viewport = getVisualViewport(); + const projectedX = currentX + velocityX * TOOLBAR_VELOCITY_MULTIPLIER_MS; + const projectedY = currentY + velocityY * TOOLBAR_VELOCITY_MULTIPLIER_MS; + const centerX = projectedX + elementWidth / 2; + const centerY = projectedY + elementHeight / 2; + const distanceToTop = centerY - viewport.offsetTop; + const distanceToBottom = viewport.offsetTop + viewport.height - centerY; + const distanceToLeft = centerX - viewport.offsetLeft; + const distanceToRight = viewport.offsetLeft + viewport.width - centerX; + const minimumDistance = Math.min( + distanceToTop, + distanceToBottom, + distanceToLeft, + distanceToRight, + ); + let edge: SnapEdge = "bottom"; + if (minimumDistance === distanceToTop) edge = "top"; + else if (minimumDistance === distanceToLeft) edge = "left"; + else if (minimumDistance === distanceToRight) edge = "right"; + const bounds = getViewportBounds(elementWidth, elementHeight); + + return { + edge, + x: + edge === "left" + ? bounds.minimumX + : edge === "right" + ? bounds.maximumX + : clampToRange(projectedX, bounds.minimumX, bounds.maximumX), + y: + edge === "top" + ? bounds.minimumY + : edge === "bottom" + ? bounds.maximumY + : clampToRange(projectedY, bounds.minimumY, bounds.maximumY), + }; +}; diff --git a/packages/scan/src/web/utils/watch-app-theme.ts b/packages/scan/src/web/utils/watch-app-theme.ts new file mode 100644 index 00000000..439602b1 --- /dev/null +++ b/packages/scan/src/web/utils/watch-app-theme.ts @@ -0,0 +1,80 @@ +import { RGB_CHANNEL_MAX, THEME_DARK_LUMINANCE_THRESHOLD } from "../constants"; + +type AppTheme = "dark" | "light"; + +const THEME_ATTRIBUTES = [ + "class", + "style", + "data-theme", + "data-mode", + "data-color-scheme", + "data-bs-theme", + "data-mui-color-scheme", +] as const; + +const getMarkerTheme = (element: HTMLElement): AppTheme | undefined => { + if (element.classList.contains("dark")) return "dark"; + if (element.classList.contains("light")) return "light"; + + for (const attribute of THEME_ATTRIBUTES) { + const value = element.getAttribute(attribute)?.toLowerCase(); + if (value === "dark" || value === "light") return value; + } +}; + +const linearizeChannel = (channel: number): number => { + const normalized = channel / RGB_CHANNEL_MAX; + return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; +}; + +const getBackgroundTheme = (element: HTMLElement): AppTheme | undefined => { + const color = getComputedStyle(element).backgroundColor; + const channels = color.match(/[\d.]+/g)?.map(Number); + if (!channels || channels.length < 3 || channels[3] === 0) return; + const luminance = + linearizeChannel(channels[0]) * 0.2126 + + linearizeChannel(channels[1]) * 0.7152 + + linearizeChannel(channels[2]) * 0.0722; + return luminance < THEME_DARK_LUMINANCE_THRESHOLD ? "dark" : "light"; +}; + +const detectAppTheme = (): AppTheme => + getMarkerTheme(document.documentElement) ?? + (document.body ? getMarkerTheme(document.body) : undefined) ?? + (document.body ? getBackgroundTheme(document.body) : undefined) ?? + getBackgroundTheme(document.documentElement) ?? + "light"; + +export const watchAppTheme = (host: HTMLElement): (() => void) => { + let frameId: number | undefined; + + const applyTheme = () => { + const appTheme = detectAppTheme(); + host.dataset.reactScanTheme = appTheme === "dark" ? "light" : "dark"; + }; + const scheduleThemeUpdate = () => { + cancelAnimationFrame(frameId ?? 0); + frameId = requestAnimationFrame(applyTheme); + }; + + applyTheme(); + const observer = new MutationObserver(scheduleThemeUpdate); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: [...THEME_ATTRIBUTES], + }); + if (document.body) { + observer.observe(document.body, { + attributes: true, + attributeFilter: [...THEME_ATTRIBUTES], + }); + } + const colorScheme = window.matchMedia("(prefers-color-scheme: dark)"); + colorScheme.addEventListener("change", scheduleThemeUpdate); + + return () => { + observer.disconnect(); + colorScheme.removeEventListener("change", scheduleThemeUpdate); + cancelAnimationFrame(frameId ?? 0); + }; +}; diff --git a/packages/scan/src/web/views/index.tsx b/packages/scan/src/web/views/index.tsx index 54816f78..c83388a7 100644 --- a/packages/scan/src/web/views/index.tsx +++ b/packages/scan/src/web/views/index.tsx @@ -1,98 +1,69 @@ -import { type ReadonlySignal, computed } from '@preact/signals'; -import type { ReactNode } from 'preact/compat'; -import { Store } from '~core/index'; -import { signalWidgetViews } from '~web/state'; -import { cn } from '~web/utils/helpers'; -import { Header } from '~web/widget/header'; -import { ViewInspector } from './inspector'; -import { Toolbar } from './toolbar'; -import { NotificationWrapper } from './notifications/notifications'; +import { createMemo, type Accessor, type JSX } from "solid-js"; +import { getInspectState } from "../../core/native-state"; +import { getWidgetView } from "../state"; +import { cn } from "../utils/helpers"; +import { Header } from "../widget/header"; +import { ViewInspector } from "./inspector"; +import { NotificationWrapper } from "./notifications/notifications"; -const isInspecting = computed( - () => Store.inspectState.value.kind === 'inspecting', -); +const isInspecting = () => getInspectState().kind === "inspecting"; -const headerClassName = computed(() => +const headerClassName = () => cn( - 'relative', - 'flex-1', - 'flex flex-col', - 'rounded-t-lg', - 'overflow-hidden', - 'opacity-100', - 'transition-[opacity]', - isInspecting.value && 'opacity-0 duration-0 delay-0', - ), -); - -const isInspectorViewOpen = computed( - () => signalWidgetViews.value.view === 'inspector', -); -const isNotificationsViewOpen = computed( - () => signalWidgetViews.value.view === 'notifications', -); + "relative flex flex-1 flex-col overflow-hidden rounded-xl opacity-100", + "transition-opacity", + isInspecting() && "opacity-0 duration-0 delay-0", + ); -export const Content = () => { - return ( -

-
-
-
- - - +const isInspectorViewOpen = () => getWidgetView().view === "inspector"; +const isNotificationsViewOpen = () => getWidgetView().view === "notifications"; - - - -
+export const Content = () => ( +
+
+
+
+ + + + + +
-
- ); -}; +
+); interface ContentViewProps { - isOpen: ReadonlySignal; - children: ReactNode; + isOpen: Accessor; + children: JSX.Element; } -const ContentView = ({ isOpen, children }: ContentViewProps) => { +const ContentView = (props: ContentViewProps) => { + const className = createMemo(() => + cn( + "flex-1 opacity-0 overflow-y-auto overflow-x-hidden", + "transition-opacity delay-0 pointer-events-none", + props.isOpen() && "opacity-100 delay-150 pointer-events-auto", + ), + ); + return ( -
-
{children}
+
+
{props.children}
); }; diff --git a/packages/scan/src/web/views/inspector/components-tree/index.tsx b/packages/scan/src/web/views/inspector/components-tree/index.tsx index 0995a8a2..8b83854d 100644 --- a/packages/scan/src/web/views/inspector/components-tree/index.tsx +++ b/packages/scan/src/web/views/inspector/components-tree/index.tsx @@ -1,37 +1,36 @@ import { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'preact/hooks'; -import { Store } from '~core/index'; -import { getRenderData } from '~core/instrumentation'; -import { Icon } from '~web/components/icon'; -import { - LOCALSTORAGE_KEY, - MIN_CONTAINER_WIDTH, -} from '~web/constants'; -import { useVirtualList } from '~web/hooks/use-virtual-list'; -import { signalWidget } from '~web/state'; -import { - cn, - getExtendedDisplayName, - saveLocalStorage, -} from '~web/utils/helpers'; -import { getFiberPath } from '~web/utils/pin'; -import { inspectorUpdateSignal } from '../states'; + For, + Show, + createEffect, + createMemo, + createSignal, + on, + onCleanup, + onMount, +} from "solid-js"; +import { getInspectState, setInspectState } from "../../../../core/native-state"; +import { getRenderData } from "../../../../core/instrumentation"; +import { Icon } from "../../../components/icon"; +import { LOCALSTORAGE_KEY, MIN_CONTAINER_WIDTH } from "../../../constants"; +import { getWidgetState, setWidgetState } from "../../../state"; +import { cn, getExtendedDisplayName, saveLocalStorage } from "../../../utils/helpers"; +import { getFiberPath } from "../../../utils/pin"; +import { getInspectorUpdateVersion } from "../states"; import { type InspectableElement, getCompositeComponentFromElement, getInspectableElements, -} from '../utils'; +} from "../utils"; import { type FlattenedNode, + type SearchState, type TreeNode, - searchState, - signalSkipTreeUpdate, -} from './state'; + getSearchState, + getShouldSkipTreeUpdate, + setSearchState, + setShouldSkipTreeUpdate, +} from "./state"; +import { createVirtualList } from "./virtual-list"; const flattenTree = ( nodes: TreeNode[], @@ -39,13 +38,9 @@ const flattenTree = ( parentPath: string | null = null, ): FlattenedNode[] => { return nodes.reduce((acc, node, index) => { - const nodePath = node.element - ? getFiberPath(node.fiber) - : `${parentPath}-${index}`; + const nodePath = node.element ? getFiberPath(node.fiber) : `${parentPath}-${index}`; - const renderData = node.fiber?.type - ? getRenderData(node.fiber) - : undefined; + const renderData = node.fiber?.type ? getRenderData(node.fiber) : undefined; const flatNode: FlattenedNode = { ...node, @@ -80,10 +75,7 @@ const calculateIndentSize = (containerWidth: number, maxDepth: number) => { if (availableSpace < MIN_TOTAL_INDENT) return MIN_INDENT; - const targetTotalIndent = Math.min( - availableSpace * 0.3, - maxDepth * MAX_INDENT, - ); + const targetTotalIndent = Math.min(availableSpace * 0.3, maxDepth * MAX_INDENT); const baseIndent = targetTotalIndent / maxDepth; return Math.max(MIN_INDENT, Math.min(MAX_INDENT, baseIndent)); @@ -96,17 +88,17 @@ interface TreeNodeItemProps { isCollapsed: boolean; handleTreeNodeClick: (e: Event) => void; handleTreeNodeToggle: (e: Event) => void; - searchValue: typeof searchState.value; + searchValue: SearchState; } -const VALID_TYPES = ['memo', 'forwardRef', 'lazy', 'suspense']; +const VALID_TYPES = ["memo", "forwardRef", "lazy", "suspense"]; const parseTypeSearch = (query: string) => { const typeMatch = query.match(/\[(.*?)\]/); if (!typeMatch) return null; const typeSearches: string[] = []; - const parts = typeMatch[1].split(','); + const parts = typeMatch[1].split(","); for (const part of parts) { const trimmed = part.trim().toLowerCase(); if (trimmed) typeSearches.push(trimmed); @@ -131,10 +123,7 @@ const isValidTypeSearch = (typeSearches: string[]) => { return true; }; -const matchesTypeSearch = ( - typeSearches: string[], - wrapperTypes: Array<{ type: string }>, -) => { +const matchesTypeSearch = (typeSearches: string[], wrapperTypes: Array<{ type: string }>) => { if (typeSearches.length === 0) return true; if (!wrapperTypes.length) return false; @@ -151,75 +140,74 @@ const matchesTypeSearch = ( return true; }; -const useNodeHighlighting = ( - node: FlattenedNode, - searchValue: typeof searchState.value, -) => { - return useMemo(() => { - const { query, matches } = searchValue; - const isMatch = matches.some((match) => match.nodeId === node.nodeId); +const createNodeHighlighting = (node: () => FlattenedNode, searchValue: () => SearchState) => { + return createMemo(() => { + const currentNode = node(); + const { query, matches } = searchValue(); + const isMatch = matches.some((match) => match.nodeId === currentNode.nodeId); const typeSearches = parseTypeSearch(query) || []; - const searchQuery = query ? query.replace(/\[.*?\]/, '').trim() : ''; + const searchQuery = query ? query.replace(/\[.*?\]/, "").trim() : ""; if (!query || !isMatch) { return { - highlightedText: {node.label}, + highlightedText: {currentNode.label}, typeHighlight: false, }; } let matchesType = true; if (typeSearches.length > 0) { - if (!node.fiber) { + if (!currentNode.fiber) { matchesType = false; } else { - const { wrapperTypes } = getExtendedDisplayName(node.fiber); + const { wrapperTypes } = getExtendedDisplayName(currentNode.fiber); matchesType = matchesTypeSearch(typeSearches, wrapperTypes); } } - let textContent = {node.label}; + let textContent = {currentNode.label}; if (searchQuery) { try { - if (searchQuery.startsWith('/') && searchQuery.endsWith('/')) { + if (searchQuery.startsWith("/") && searchQuery.endsWith("/")) { const pattern = searchQuery.slice(1, -1); - const regex = new RegExp(`(${pattern})`, 'i'); - const parts = node.label.split(regex); + const regex = new RegExp(`(${pattern})`, "i"); + const parts = currentNode.label.split(regex); textContent = ( - - {parts.map((part, index) => - regex.test(part) ? ( - - {part} - - ) : ( - part - ), - )} + + + {(part, index) => + regex.test(part) ? ( + + {part} + + ) : ( + part + ) + } + ); } else { - const lowerLabel = node.label.toLowerCase(); + const lowerLabel = currentNode.label.toLowerCase(); const lowerQuery = searchQuery.toLowerCase(); const index = lowerLabel.indexOf(lowerQuery); if (index >= 0) { textContent = ( - - {node.label.slice(0, index)} - - {node.label.slice(index, index + searchQuery.length)} + + {currentNode.label.slice(0, index)} + + {currentNode.label.slice(index, index + searchQuery.length)} - {node.label.slice(index + searchQuery.length)} + {currentNode.label.slice(index + searchQuery.length)} ); } @@ -231,179 +219,170 @@ const useNodeHighlighting = ( highlightedText: textContent, typeHighlight: matchesType && typeSearches.length > 0, }; - }, [node.label, node.nodeId, node.fiber, searchValue]); + }); }; const formatTime = (time: number) => { if (time > 0) { if (time < 0.1 - Number.EPSILON) { - return '< 0.1'; + return "< 0.1"; } if (time < 1000) { return Number(time.toFixed(1)).toString(); } return `${(time / 1000).toFixed(1)}k`; } - return '0'; + return "0"; }; -const TreeNodeItem = ({ - node, - nodeIndex, - hasChildren, - isCollapsed, - handleTreeNodeClick, - handleTreeNodeToggle, - searchValue, -}: TreeNodeItemProps) => { - const refRenderCount = useRef(null); - const refPrevRenderCount = useRef(node.renderData?.renderCount ?? 0); - - const { highlightedText, typeHighlight } = useNodeHighlighting( - node, - searchValue, +const TreeNodeItem = (props: TreeNodeItemProps) => { + let renderCountElement: HTMLSpanElement | undefined; + let previousRenderCount = props.node.renderData?.renderCount ?? 0; + + const nodeHighlighting = createNodeHighlighting( + () => props.node, + () => props.searchValue, ); - useEffect(() => { - const currentRenderCount = node.renderData?.renderCount; - const element = refRenderCount.current; + createEffect(() => { + const currentRenderCount = props.node.renderData?.renderCount; + const element = renderCountElement; if ( !element || - !refPrevRenderCount.current || + !previousRenderCount || !currentRenderCount || - refPrevRenderCount.current === currentRenderCount + previousRenderCount === currentRenderCount ) { return; } - element.classList.remove('count-flash'); + element.classList.remove("count-flash"); void element.offsetWidth; - element.classList.add('count-flash'); + element.classList.add("count-flash"); - refPrevRenderCount.current = currentRenderCount; - }, [node.renderData?.renderCount]); + previousRenderCount = currentRenderCount; + }); - const renderTimeInfo = useMemo(() => { - if (!node.renderData) return null; - const { selfTime, totalTime, renderCount } = node.renderData; + const renderTimeInfo = createMemo(() => { + if (!props.node.renderData) return null; + const { selfTime, totalTime, renderCount } = props.node.renderData; if (!renderCount) { return null; } return ( - + ×{renderCount} ); - }, [node.renderData]); + }); - const componentTypes = useMemo(() => { - if (!node.fiber) return null; - const { wrapperTypes } = getExtendedDisplayName(node.fiber); + const componentTypes = createMemo(() => { + if (!props.node.fiber) return null; + const { wrapperTypes } = getExtendedDisplayName(props.node.fiber); const firstWrapperType = wrapperTypes[0]; return ( - {firstWrapperType && ( - <> - - {firstWrapperType.type} - - {firstWrapperType.compiler && ( - - )} - - )} - {wrapperTypes.length > 1 && `×${wrapperTypes.length}`} - {renderTimeInfo} + + {(wrapperType) => ( + <> + + {wrapperType().type} + + + + + + )} + + 1}>×{wrapperTypes.length} + {renderTimeInfo()} ); - }, [node.fiber, typeHighlight, renderTimeInfo]); + }); return ( - - {highlightedText} - {componentTypes} - + {nodeHighlighting().highlightedText} + {componentTypes()} +
); }; export const ComponentsTree = () => { - const refContainer = useRef(null); - const refMainContainer = useRef(null); - const refSearchInputContainer = useRef(null); - const refSearchInput = useRef(null); - const refSelectedElement = useRef(null); - const refMaxTreeDepth = useRef(0); - const refIsHovering = useRef(false); - const refIsResizing = useRef(false); - const refResizeHandle = useRef(null); - - const [flattenedNodes, setFlattenedNodes] = useState([]); - const [collapsedNodes, setCollapsedNodes] = useState>(new Set()); - const [selectedIndex, setSelectedIndex] = useState( - undefined, - ); - const [searchValue, setSearchValue] = useState(searchState.value); - - const visibleNodes = useMemo(() => { + let containerElement: HTMLDivElement | undefined; + let mainContainerElement: HTMLDivElement | undefined; + let searchInputContainerElement: HTMLDivElement | undefined; + let searchInputElement: HTMLInputElement | undefined; + let selectedElement: HTMLElement | null = null; + let maxTreeDepth = 0; + let isHovering = false; + let isResizing = false; + let resizeHandleElement: HTMLDivElement | undefined; + + const [flattenedNodes, setFlattenedNodes] = createSignal([]); + const [collapsedNodes, setCollapsedNodes] = createSignal>(new Set()); + const [selectedIndex, setSelectedIndex] = createSignal(undefined); + + const visibleNodes = createMemo(() => { const visible: FlattenedNode[] = []; - const nodes = flattenedNodes; + const nodes = flattenedNodes(); const nodeMap = new Map(nodes.map((node) => [node.nodeId, node])); for (const node of nodes) { @@ -414,7 +393,7 @@ export const ComponentsTree = () => { const parent = nodeMap.get(currentNode.parentId); if (!parent) break; - if (collapsedNodes.has(parent.nodeId)) { + if (collapsedNodes().has(parent.nodeId)) { isVisible = false; break; } @@ -427,344 +406,304 @@ export const ComponentsTree = () => { } return visible; - }, [collapsedNodes, flattenedNodes]); + }); const ITEM_HEIGHT = 28; - const { virtualItems, totalSize } = useVirtualList({ - count: visibleNodes.length, - getScrollElement: () => refContainer.current, + const { virtualItems, totalSize } = createVirtualList({ + count: () => visibleNodes().length, + getScrollElement: () => containerElement, estimateSize: () => ITEM_HEIGHT, overscan: 5, }); - const handleElementClick = useCallback( - (element: HTMLElement) => { - refIsHovering.current = true; - refSearchInput.current?.blur(); - signalSkipTreeUpdate.value = true; + const handleElementClick = (element: HTMLElement) => { + isHovering = true; + searchInputElement?.blur(); + setShouldSkipTreeUpdate(true); - const { parentCompositeFiber } = - getCompositeComponentFromElement(element); - if (!parentCompositeFiber) return; + const { parentCompositeFiber } = getCompositeComponentFromElement(element); + if (!parentCompositeFiber) return; - Store.inspectState.value = { - kind: 'focused', - focusedDomElement: element, - fiber: parentCompositeFiber, - }; + setInspectState({ + kind: "focused", + focusedDomElement: element, + fiber: parentCompositeFiber, + }); - const nodeIndex = visibleNodes.findIndex( - (node) => node.element === element, - ); - if (nodeIndex !== -1) { - setSelectedIndex(nodeIndex); - const itemTop = nodeIndex * ITEM_HEIGHT; - const container = refContainer.current; - if (container) { - const containerHeight = container.clientHeight; - const scrollTop = container.scrollTop; - - if ( - itemTop < scrollTop || - itemTop + ITEM_HEIGHT > scrollTop + containerHeight - ) { - container.scrollTo({ - top: Math.max(0, itemTop - containerHeight / 2), - behavior: 'instant', - }); - } + const nodeIndex = visibleNodes().findIndex((node) => node.element === element); + if (nodeIndex !== -1) { + setSelectedIndex(nodeIndex); + const itemTop = nodeIndex * ITEM_HEIGHT; + const container = containerElement; + if (container) { + const containerHeight = container.clientHeight; + const scrollTop = container.scrollTop; + + if (itemTop < scrollTop || itemTop + ITEM_HEIGHT > scrollTop + containerHeight) { + container.scrollTo({ + top: Math.max(0, itemTop - containerHeight / 2), + behavior: "instant", + }); } } - }, - [visibleNodes], - ); - - const handleTreeNodeClick = useCallback( - (e: Event) => { - const target = e.currentTarget as HTMLElement; - const index = Number(target.dataset.index); - if (Number.isNaN(index)) return; - const element = visibleNodes[index].element; - if (!element) return; - handleElementClick(element); - }, - [visibleNodes, handleElementClick], - ); - - const handleToggle = useCallback((nodeId: string) => { - setCollapsedNodes((prev) => { - const next = new Set(prev); - if (next.has(nodeId)) { - next.delete(nodeId); + } + }; + + const handleTreeNodeClick = (event: Event) => { + const target = event.currentTarget as HTMLElement; + const index = Number(target.dataset.index); + if (Number.isNaN(index)) return; + const element = visibleNodes()[index].element; + if (!element) return; + handleElementClick(element); + }; + + const handleToggle = (nodeId: string) => { + setCollapsedNodes((previousNodes) => { + const nextNodes = new Set(previousNodes); + if (nextNodes.has(nodeId)) { + nextNodes.delete(nodeId); } else { - next.add(nodeId); + nextNodes.add(nodeId); } - return next; + return nextNodes; }); - }, []); - - const handleTreeNodeToggle = useCallback( - (e: Event) => { - e.stopPropagation(); - const target = e.target as HTMLElement; - const index = Number(target.dataset.index); - if (Number.isNaN(index)) return; - const nodeId = visibleNodes[index].nodeId; - handleToggle(nodeId); - }, - [visibleNodes, handleToggle], - ); - - const handleOnChangeSearch = useCallback( - (query: string) => { - refSearchInputContainer.current?.classList.remove('!border-red-500'); - const matches: FlattenedNode[] = []; + }; + + const handleTreeNodeToggle = (event: Event) => { + event.stopPropagation(); + const target = event.currentTarget as HTMLElement; + const index = Number(target.dataset.index); + if (Number.isNaN(index)) return; + const nodeId = visibleNodes()[index].nodeId; + handleToggle(nodeId); + }; + + const handleOnChangeSearch = (query: string) => { + searchInputContainerElement?.classList.remove("!border-red-500"); + const matches: FlattenedNode[] = []; + + if (!query) { + setSearchState({ query, matches, currentMatchIndex: -1 }); + return; + } - if (!query) { - searchState.value = { query, matches, currentMatchIndex: -1 }; + if (query.includes("[") && !query.includes("]")) { + if (query.length > query.indexOf("[") + 1) { + searchInputContainerElement?.classList.add("!border-red-500"); return; } + } - if (query.includes('[') && !query.includes(']')) { - if (query.length > query.indexOf('[') + 1) { - refSearchInputContainer.current?.classList.add('!border-red-500'); - return; - } - } - - const typeSearches = parseTypeSearch(query) || []; - if (query.includes('[')) { - if (!isValidTypeSearch(typeSearches)) { - refSearchInputContainer.current?.classList.add('!border-red-500'); - return; - } + const typeSearches = parseTypeSearch(query) || []; + if (query.includes("[")) { + if (!isValidTypeSearch(typeSearches)) { + searchInputContainerElement?.classList.add("!border-red-500"); + return; } + } - const searchQuery = query.replace(/\[.*?\]/, '').trim(); - const isRegex = /^\/.*\/$/.test(searchQuery); - let matchesLabel = (_label: string) => false; + const searchQuery = query.replace(/\[.*?\]/, "").trim(); + const isRegex = /^\/.*\/$/.test(searchQuery); + let matchesLabel = (_label: string) => false; - if (searchQuery.startsWith('/') && !isRegex) { - if (searchQuery.length > 1) { - refSearchInputContainer.current?.classList.add('!border-red-500'); - return; - } + if (searchQuery.startsWith("/") && !isRegex) { + if (searchQuery.length > 1) { + searchInputContainerElement?.classList.add("!border-red-500"); + return; } + } - if (isRegex) { - try { - const pattern = searchQuery.slice(1, -1); - const regex = new RegExp(pattern, 'i'); - matchesLabel = (label: string) => regex.test(label); - } catch { - refSearchInputContainer.current?.classList.add('!border-red-500'); - return; - } - } else if (searchQuery) { - const lowerQuery = searchQuery.toLowerCase(); - matchesLabel = (label: string) => - label.toLowerCase().includes(lowerQuery); + if (isRegex) { + try { + const pattern = searchQuery.slice(1, -1); + const regex = new RegExp(pattern, "i"); + matchesLabel = (label: string) => regex.test(label); + } catch { + searchInputContainerElement?.classList.add("!border-red-500"); + return; } + } else if (searchQuery) { + const lowerQuery = searchQuery.toLowerCase(); + matchesLabel = (label: string) => label.toLowerCase().includes(lowerQuery); + } - for (const node of flattenedNodes) { - let matchesSearch = true; + for (const node of flattenedNodes()) { + let matchesSearch = true; - if (searchQuery) { - matchesSearch = matchesLabel(node.label); - } - - if (matchesSearch && typeSearches.length > 0) { - if (!node.fiber) { - matchesSearch = false; - } else { - const { wrapperTypes } = getExtendedDisplayName(node.fiber); - matchesSearch = matchesTypeSearch(typeSearches, wrapperTypes); - } - } - - if (matchesSearch) { - matches.push(node); - } + if (searchQuery) { + matchesSearch = matchesLabel(node.label); } - searchState.value = { - query, - matches, - currentMatchIndex: matches.length > 0 ? 0 : -1, - }; - - if (matches.length > 0) { - const firstMatch = matches[0]; - const nodeIndex = visibleNodes.findIndex( - (node) => node.nodeId === firstMatch.nodeId, - ); - if (nodeIndex !== -1) { - const itemTop = nodeIndex * ITEM_HEIGHT; - const container = refContainer.current; - if (container) { - const containerHeight = container.clientHeight; - container.scrollTo({ - top: Math.max(0, itemTop - containerHeight / 2), - behavior: 'instant', - }); - } + if (matchesSearch && typeSearches.length > 0) { + if (!node.fiber) { + matchesSearch = false; + } else { + const { wrapperTypes } = getExtendedDisplayName(node.fiber); + matchesSearch = matchesTypeSearch(typeSearches, wrapperTypes); } } - }, - [flattenedNodes, visibleNodes], - ); - - const handleInputChange = useCallback( - (e: Event) => { - const target = e.currentTarget as HTMLInputElement; - if (!target) return; - handleOnChangeSearch(target.value); - }, - [handleOnChangeSearch], - ); - const navigateSearch = useCallback( - (direction: 'next' | 'prev') => { - const { matches, currentMatchIndex } = searchState.value; - if (matches.length === 0) return; - - const newIndex = - direction === 'next' - ? (currentMatchIndex + 1) % matches.length - : (currentMatchIndex - 1 + matches.length) % matches.length; + if (matchesSearch) { + matches.push(node); + } + } - searchState.value = { - ...searchState.value, - currentMatchIndex: newIndex, - }; + setSearchState({ + query, + matches, + currentMatchIndex: matches.length > 0 ? 0 : -1, + }); - const currentMatch = matches[newIndex]; - const nodeIndex = visibleNodes.findIndex( - (node) => node.nodeId === currentMatch.nodeId, - ); + if (matches.length > 0) { + const firstMatch = matches[0]; + const nodeIndex = visibleNodes().findIndex((node) => node.nodeId === firstMatch.nodeId); if (nodeIndex !== -1) { - setSelectedIndex(nodeIndex); const itemTop = nodeIndex * ITEM_HEIGHT; - const container = refContainer.current; + const container = containerElement; if (container) { const containerHeight = container.clientHeight; container.scrollTo({ top: Math.max(0, itemTop - containerHeight / 2), - behavior: 'instant', + behavior: "instant", }); } } - }, - [visibleNodes], - ); + } + }; + + const handleInputChange = (event: Event) => { + const target = event.currentTarget as HTMLInputElement; + if (!target) return; + handleOnChangeSearch(target.value); + }; + + const navigateSearch = (direction: "next" | "prev") => { + const { matches, currentMatchIndex } = getSearchState(); + if (matches.length === 0) return; + + const newIndex = + direction === "next" + ? (currentMatchIndex + 1) % matches.length + : (currentMatchIndex - 1 + matches.length) % matches.length; + + setSearchState({ + ...getSearchState(), + currentMatchIndex: newIndex, + }); - const updateContainerWidths = useCallback((width: number) => { - if (refMainContainer.current) { - refMainContainer.current.style.width = `${width}px`; + const currentMatch = matches[newIndex]; + const nodeIndex = visibleNodes().findIndex((node) => node.nodeId === currentMatch.nodeId); + if (nodeIndex !== -1) { + setSelectedIndex(nodeIndex); + const itemTop = nodeIndex * ITEM_HEIGHT; + const container = containerElement; + if (container) { + const containerHeight = container.clientHeight; + container.scrollTo({ + top: Math.max(0, itemTop - containerHeight / 2), + behavior: "instant", + }); + } } - if (refContainer.current) { - refContainer.current.style.width = `${width}px`; - const indentSize = calculateIndentSize(width, refMaxTreeDepth.current); - refContainer.current.style.setProperty( - '--indentation-size', - `${indentSize}px`, - ); + }; + + const updateContainerWidths = (width: number) => { + if (mainContainerElement) { + mainContainerElement.style.width = `${width}px`; } - }, []); + if (containerElement) { + containerElement.style.width = `${width}px`; + const indentSize = calculateIndentSize(width, maxTreeDepth); + containerElement.style.setProperty("--indentation-size", `${indentSize}px`); + } + }; - const updateResizeDirection = useCallback((width: number) => { - if (!refResizeHandle.current) return; + const updateResizeDirection = (width: number) => { + if (!resizeHandleElement) return; - const parentWidth = signalWidget.value.dimensions.width; - const maxWidth = Math.floor(parentWidth - (MIN_CONTAINER_WIDTH / 2)); + const parentWidth = getWidgetState().dimensions.width; + const maxWidth = Math.floor(parentWidth - MIN_CONTAINER_WIDTH / 2); - refResizeHandle.current.classList.remove( - 'cursor-ew-resize', - 'cursor-w-resize', - 'cursor-e-resize', - ); + resizeHandleElement.classList.remove("cursor-ew-resize", "cursor-w-resize", "cursor-e-resize"); if (width <= MIN_CONTAINER_WIDTH) { - refResizeHandle.current.classList.add('cursor-w-resize'); + resizeHandleElement.classList.add("cursor-w-resize"); } else if (width >= maxWidth) { - refResizeHandle.current.classList.add('cursor-e-resize'); + resizeHandleElement.classList.add("cursor-e-resize"); } else { - refResizeHandle.current.classList.add('cursor-ew-resize'); + resizeHandleElement.classList.add("cursor-ew-resize"); } - }, []); + }; - const handleResize = useCallback( - (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + let cleanupResizeListeners = () => {}; + const handleResize = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); - if (!refContainer.current) return; - refContainer.current.style.setProperty('pointer-events', 'none'); + if (!containerElement) return; + containerElement.style.setProperty("pointer-events", "none"); - refIsResizing.current = true; + isResizing = true; - const startX = e.clientX; - const startWidth = refContainer.current.offsetWidth; - const parentWidth = signalWidget.value.dimensions.width; - const maxWidth = Math.floor(parentWidth - (MIN_CONTAINER_WIDTH / 2)); + const startX = event.clientX; + const startWidth = containerElement.offsetWidth; + const parentWidth = getWidgetState().dimensions.width; + const maxWidth = Math.floor(parentWidth - MIN_CONTAINER_WIDTH / 2); - updateResizeDirection(startWidth); + updateResizeDirection(startWidth); - const handlePointerMove = (e: PointerEvent) => { - const delta = startX - e.clientX; - const newWidth = startWidth + delta; - updateResizeDirection(newWidth); + const handlePointerMove = (e: PointerEvent) => { + const delta = startX - e.clientX; + const newWidth = startWidth + delta; + updateResizeDirection(newWidth); - const clampedWidth = Math.min( - maxWidth, - Math.max(MIN_CONTAINER_WIDTH, newWidth), - ); - updateContainerWidths(clampedWidth); - }; + const clampedWidth = Math.min(maxWidth, Math.max(MIN_CONTAINER_WIDTH, newWidth)); + updateContainerWidths(clampedWidth); + }; - const handlePointerUp = () => { - if (!refContainer.current) return; - refContainer.current.style.removeProperty('pointer-events'); - document.removeEventListener('pointermove', handlePointerMove); - document.removeEventListener('pointerup', handlePointerUp); - - signalWidget.value = { - ...signalWidget.value, - componentsTree: { - ...signalWidget.value.componentsTree, - width: refContainer.current.offsetWidth, - }, - }; - - saveLocalStorage(LOCALSTORAGE_KEY, signalWidget.value); - refIsResizing.current = false; - }; + const handlePointerUp = () => { + if (!containerElement) return; + containerElement.style.removeProperty("pointer-events"); + cleanupResizeListeners(); + + setWidgetState((state) => ({ + ...state, + componentsTree: { + ...state.componentsTree, + width: containerElement.offsetWidth, + }, + })); + + saveLocalStorage(LOCALSTORAGE_KEY, getWidgetState()); + isResizing = false; + }; - document.addEventListener('pointermove', handlePointerMove); - document.addEventListener('pointerup', handlePointerUp); - }, - [updateContainerWidths, updateResizeDirection], + cleanupResizeListeners(); + cleanupResizeListeners = () => { + document.removeEventListener("pointermove", handlePointerMove); + document.removeEventListener("pointerup", handlePointerUp); + }; + document.addEventListener("pointermove", handlePointerMove); + document.addEventListener("pointerup", handlePointerUp); + }; + onCleanup(() => cleanupResizeListeners()); + + createEffect( + on(getWidgetState, () => { + if (!containerElement) return; + updateResizeDirection(containerElement.offsetWidth); + }), ); - useEffect(() => { - if (!refContainer.current) return; - const currentWidth = refContainer.current.offsetWidth; - updateResizeDirection(currentWidth); - - return signalWidget.subscribe(() => { - if (!refContainer.current) return; - updateResizeDirection(refContainer.current.offsetWidth); - }); - }, [updateResizeDirection]); - - const onPointerLeave = useCallback(() => { - refIsHovering.current = false; - }, []); + const onPointerLeave = () => { + isHovering = false; + }; - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { + onMount(() => { let isInitialTreeBuild = true; + let initialScrollTimer: ReturnType | undefined; const buildTreeFromElements = (elements: Array) => { const nodeMap = new Map(); const rootNodes: TreeNode[] = []; @@ -776,7 +715,7 @@ export const ComponentsTree = () => { const { name: componentName, wrappers } = getExtendedDisplayName(fiber); if (componentName) { if (wrappers.length > 0) { - title = `${wrappers.join('(')}(${componentName})${')'.repeat(wrappers.length)}`; + title = `${wrappers.join("(")}(${componentName})${")".repeat(wrappers.length)}`; } else { title = componentName; } @@ -816,7 +755,7 @@ export const ComponentsTree = () => { }; const updateTree = () => { - const element = refSelectedElement.current; + const element = selectedElement; if (!element) return; const inspectableElements = getInspectableElements(); @@ -825,24 +764,22 @@ export const ComponentsTree = () => { if (tree.length > 0) { const flattened = flattenTree(tree); const newMaxDepth = getMaxDepth(flattened); - refMaxTreeDepth.current = newMaxDepth; + maxTreeDepth = newMaxDepth; - updateContainerWidths(signalWidget.value.componentsTree.width); + updateContainerWidths(getWidgetState().componentsTree.width); setFlattenedNodes(flattened); if (isInitialTreeBuild) { isInitialTreeBuild = false; - const focusedIndex = flattened.findIndex( - (node) => node.element === element, - ); + const focusedIndex = flattened.findIndex((node) => node.element === element); if (focusedIndex !== -1) { const itemTop = focusedIndex * ITEM_HEIGHT; - const container = refContainer.current; + const container = containerElement; if (container) { - setTimeout(() => { + initialScrollTimer = setTimeout(() => { container.scrollTo({ top: itemTop, - behavior: 'instant', + behavior: "instant", }); }, 96); } @@ -851,89 +788,101 @@ export const ComponentsTree = () => { } }; - const unsubscribeStore = Store.inspectState.subscribe((state) => { - if (state.kind === 'focused') { - if (signalSkipTreeUpdate.value) { - return; - } + createEffect( + on( + getInspectState, + (state) => { + if (state.kind === "focused") { + if (getShouldSkipTreeUpdate()) { + return; + } - handleOnChangeSearch(''); - refSelectedElement.current = state.focusedDomElement as HTMLElement; - updateTree(); - } - }); + handleOnChangeSearch(""); + selectedElement = state.focusedDomElement as HTMLElement; + updateTree(); + } + }, + { defer: true }, + ), + ); let rafId = 0; - const unsubscribeUpdates = inspectorUpdateSignal.subscribe(() => { - if (Store.inspectState.value.kind === 'focused') { - cancelAnimationFrame(rafId); - if (refIsResizing.current) return; - - rafId = requestAnimationFrame(() => { - signalSkipTreeUpdate.value = false; - updateTree(); - }); - } - }); - - return () => { - unsubscribeStore(); - unsubscribeUpdates(); + createEffect( + on( + getInspectorUpdateVersion, + () => { + if (getInspectState().kind === "focused") { + cancelAnimationFrame(rafId); + if (isResizing) return; + + rafId = requestAnimationFrame(() => { + setShouldSkipTreeUpdate(false); + updateTree(); + }); + } + }, + { defer: true }, + ), + ); - searchState.value = { - query: '', + onCleanup(() => { + setSearchState({ + query: "", matches: [], currentMatchIndex: -1, - }; - }; - }, []); + }); + cancelAnimationFrame(rafId); + clearTimeout(initialScrollTimer); + }); + }); - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!refIsHovering.current) return; + onMount(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!isHovering) return; - if (!selectedIndex) return; + const currentSelectedIndex = selectedIndex(); + if (currentSelectedIndex === undefined) return; - switch (e.key) { - case 'ArrowUp': { - e.preventDefault(); - e.stopPropagation(); + switch (event.key) { + case "ArrowUp": { + event.preventDefault(); + event.stopPropagation(); - if (selectedIndex > 0) { - const currentNode = visibleNodes[selectedIndex - 1]; + if (currentSelectedIndex > 0) { + const currentNode = visibleNodes()[currentSelectedIndex - 1]; if (currentNode?.element) { handleElementClick(currentNode.element); } } return; } - case 'ArrowDown': { - e.preventDefault(); - e.stopPropagation(); + case "ArrowDown": { + event.preventDefault(); + event.stopPropagation(); - if (selectedIndex < visibleNodes.length - 1) { - const currentNode = visibleNodes[selectedIndex + 1]; + if (currentSelectedIndex < visibleNodes().length - 1) { + const currentNode = visibleNodes()[currentSelectedIndex + 1]; if (currentNode?.element) { handleElementClick(currentNode.element); } } return; } - case 'ArrowLeft': { - e.preventDefault(); - e.stopPropagation(); + case "ArrowLeft": { + event.preventDefault(); + event.stopPropagation(); - const currentNode = visibleNodes[selectedIndex]; + const currentNode = visibleNodes()[currentSelectedIndex]; if (currentNode?.nodeId) { handleToggle(currentNode.nodeId); } return; } - case 'ArrowRight': { - e.preventDefault(); - e.stopPropagation(); + case "ArrowRight": { + event.preventDefault(); + event.stopPropagation(); - const currentNode = visibleNodes[selectedIndex]; + const currentNode = visibleNodes()[currentSelectedIndex]; if (currentNode?.nodeId) { handleToggle(currentNode.nodeId); } @@ -942,44 +891,39 @@ export const ComponentsTree = () => { } }; - document.addEventListener('keydown', handleKeyDown); - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; - }, [selectedIndex, visibleNodes, handleElementClick, handleToggle]); - - useEffect(() => { - return searchState.subscribe(setSearchValue); - }, []); + document.addEventListener("keydown", handleKeyDown); + onCleanup(() => { + document.removeEventListener("keydown", handleKeyDown); + }); + }); - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - const unsubscribe = signalWidget.subscribe((state) => { - refMainContainer.current?.style.setProperty('transition', 'width 0.1s'); + let transitionTimer: ReturnType | undefined; + createEffect( + on(getWidgetState, (state) => { + mainContainerElement?.style.setProperty("transition", "width 0.1s"); updateContainerWidths(state.componentsTree.width); - setTimeout(() => { - refMainContainer.current?.style.removeProperty('transition'); + clearTimeout(transitionTimer); + transitionTimer = setTimeout(() => { + mainContainerElement?.style.removeProperty("transition"); }, 500); - }); - return unsubscribe; - }, []); + }), + ); + onCleanup(() => { + clearTimeout(transitionTimer); + }); return ( -
-
+
+
-
-
+
+
{ - Shift + Enter → Previous match - Cmd/Ctrl + Enter → Select and focus match `} - className={cn( - 'relative', - 'flex items-center gap-x-1 px-2', - 'rounded', - 'border border-transparent', - 'focus-within:border-[#454545]', - 'bg-[#1e1e1e] text-neutral-300', - 'transition-colors', - 'whitespace-nowrap', - 'overflow-hidden', + class={cn( + "relative", + "flex items-center gap-x-1 px-2", + "rounded", + "border border-transparent", + "focus-within:border-[#454545]", + "bg-[#1e1e1e] text-neutral-300", + "transition-colors", + "whitespace-nowrap", + "overflow-hidden", )} > - -
+ +
{ e.stopPropagation(); e.currentTarget.focus(); @@ -1026,148 +970,144 @@ export const ComponentsTree = () => { e.stopPropagation(); }} onKeyDown={(e) => { - if (e.key === 'Escape') { + if (e.key === "Escape") { e.currentTarget.blur(); } - if (searchState.value.matches.length) { - if (e.key === 'Enter' && e.shiftKey) { - navigateSearch('prev'); - } else if (e.key === 'Enter') { + if (getSearchState().matches.length) { + if (e.key === "Enter" && e.shiftKey) { + navigateSearch("prev"); + } else if (e.key === "Enter") { if (e.metaKey || e.ctrlKey) { e.preventDefault(); e.stopPropagation(); handleElementClick( - searchState.value.matches[ - searchState.value.currentMatchIndex - ].element as HTMLElement, + getSearchState().matches[getSearchState().currentMatchIndex] + .element as HTMLElement, ); e.currentTarget.focus(); } else { - navigateSearch('next'); + navigateSearch("next"); } } } }} - onChange={handleInputChange} - className="absolute inset-y-0 inset-x-1" + onInput={handleInputChange} + class="absolute inset-y-0 inset-x-1" placeholder="Component name, /regex/, or [type]" />
- {searchState.value.query ? ( - <> - - {searchState.value.currentMatchIndex + 1} - {'|'} - {searchState.value.matches.length} - - {!!searchState.value.matches.length && ( - <> - - - - )} + 0}> + {flattenedNodes().length} + + } + > + + {getSearchState().currentMatchIndex + 1} + {"|"} + {getSearchState().matches.length} + + 0}> - - ) : ( - !!flattenedNodes.length && ( - - {flattenedNodes.length} - - ) - )} + + + +
-
+
- {virtualItems.map((virtualItem) => { - const node = visibleNodes[virtualItem.index]; - if (!node) return null; - - const isSelected = - Store.inspectState.value.kind === 'focused' && - node.element === Store.inspectState.value.focusedDomElement; - const isKeyboardSelected = virtualItem.index === selectedIndex; - - return ( -
-
- -
-
- ); - })} + + {(virtualItem) => { + const node = () => visibleNodes()[virtualItem.index]; + + const isSelected = () => { + const inspectState = getInspectState(); + return ( + inspectState.kind === "focused" && + node()?.element === inspectState.focusedDomElement + ); + }; + const isKeyboardSelected = () => virtualItem.index === selectedIndex(); + + return ( + + {(currentNode) => ( +
+
+ +
+
+ )} +
+ ); + }} +
diff --git a/packages/scan/src/web/views/inspector/components-tree/state.ts b/packages/scan/src/web/views/inspector/components-tree/state.ts index ed1d8eae..8b4ac35f 100644 --- a/packages/scan/src/web/views/inspector/components-tree/state.ts +++ b/packages/scan/src/web/views/inspector/components-tree/state.ts @@ -1,6 +1,6 @@ -import { signal } from "@preact/signals"; import type { Fiber } from "bippy"; -import type { RenderData } from "~core/instrumentation"; +import { createSignal } from "solid-js"; +import type { RenderData } from "../../../../core/instrumentation"; export interface TreeNode { label: string; @@ -18,14 +18,18 @@ export interface FlattenedNode extends TreeNode { fiber: Fiber; } -export const searchState = signal<{ +export interface SearchState { query: string; matches: FlattenedNode[]; currentMatchIndex: number; -}>({ - query: "", - matches: [], - currentMatchIndex: -1, -}); +} + +export const [getSearchState, setSearchState] = + /* @__PURE__ */ createSignal({ + query: "", + matches: [], + currentMatchIndex: -1, + }); -export const signalSkipTreeUpdate = /* @__PURE__ */ signal(false); +export const [getShouldSkipTreeUpdate, setShouldSkipTreeUpdate] = + /* @__PURE__ */ createSignal(false); diff --git a/packages/scan/src/web/views/inspector/components-tree/virtual-list.ts b/packages/scan/src/web/views/inspector/components-tree/virtual-list.ts new file mode 100644 index 00000000..b9082751 --- /dev/null +++ b/packages/scan/src/web/views/inspector/components-tree/virtual-list.ts @@ -0,0 +1,98 @@ +import { createMemo, createSignal, onCleanup, onMount } from "solid-js"; + +export interface VirtualItem { + key: number; + index: number; + start: number; +} + +interface VirtualListOptions { + count: () => number; + getScrollElement: () => HTMLElement | undefined; + estimateSize: () => number; + overscan: number; +} + +export const createVirtualList = (options: VirtualListOptions) => { + const [scrollTop, setScrollTop] = createSignal(0); + const [containerHeight, setContainerHeight] = createSignal(0); + const itemHeight = options.estimateSize(); + const itemCache = new Map(); + let animationFrameId: number | undefined; + + onMount(() => { + const scrollElement = options.getScrollElement(); + if (!scrollElement) return; + + const updateContainerHeight = () => { + setContainerHeight(scrollElement.getBoundingClientRect().height); + }; + const scheduleContainerHeightUpdate = () => { + if (animationFrameId !== undefined) { + cancelAnimationFrame(animationFrameId); + } + animationFrameId = requestAnimationFrame(() => { + updateContainerHeight(); + animationFrameId = undefined; + }); + }; + const handleScroll = () => { + setScrollTop(scrollElement.scrollTop); + }; + + updateContainerHeight(); + + const resizeObserver = new ResizeObserver(scheduleContainerHeightUpdate); + resizeObserver.observe(scrollElement); + scrollElement.addEventListener("scroll", handleScroll, { passive: true }); + + const mutationObserver = new MutationObserver(scheduleContainerHeightUpdate); + mutationObserver.observe(scrollElement, { + attributes: true, + childList: true, + subtree: true, + }); + + onCleanup(() => { + scrollElement.removeEventListener("scroll", handleScroll); + resizeObserver.disconnect(); + mutationObserver.disconnect(); + if (animationFrameId !== undefined) { + cancelAnimationFrame(animationFrameId); + } + }); + }); + + const visibleRange = createMemo(() => { + const start = Math.floor(scrollTop() / itemHeight); + const visibleCount = Math.ceil(containerHeight() / itemHeight); + + return { + start: Math.max(0, start - options.overscan), + end: Math.min(options.count(), start + visibleCount + options.overscan), + }; + }); + + const virtualItems = createMemo(() => { + const items: VirtualItem[] = []; + const range = visibleRange(); + for (let index = range.start; index < range.end; index++) { + let item = itemCache.get(index); + if (!item) { + item = { + key: index, + index, + start: index * itemHeight, + }; + itemCache.set(index, item); + } + items.push(item); + } + return items; + }); + + return { + virtualItems, + totalSize: () => options.count() * itemHeight, + }; +}; diff --git a/packages/scan/src/web/views/inspector/diff-value.tsx b/packages/scan/src/web/views/inspector/diff-value.tsx index 3298a7ab..2b321c1d 100644 --- a/packages/scan/src/web/views/inspector/diff-value.tsx +++ b/packages/scan/src/web/views/inspector/diff-value.tsx @@ -1,203 +1,196 @@ -import { useState } from 'preact/hooks'; -import { CopyToClipboard } from '~web/components/copy-to-clipboard'; -import { Icon } from '~web/components/icon'; -import { cn } from '~web/utils/helpers'; -import { formatForClipboard, formatValuePreview, safeGetValue } from './utils'; +import { For, Index, Show, createMemo, createSignal } from "solid-js"; +import { CopyToClipboard } from "../../components/copy-to-clipboard"; +import { Icon } from "../../components/icon"; +import { cn } from "../../utils/helpers"; +import { formatForClipboard, formatValuePreview, safeGetValue } from "./utils"; -const ArrayHeader = ({ - length, - expanded, - onToggle, - isNegative, -}: { +interface ArrayHeaderProps { length: number; expanded: boolean; onToggle: () => void; isNegative: boolean; -}) => ( -
- - Array({length}) + Array({props.length})
); -const TreeNode = ({ - value, - path, - isNegative, -}: { - value: unknown; - path: string; - isNegative: boolean; -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const canExpand = value !== null && - typeof value === 'object' && - !(value instanceof Date); - - if (!canExpand) { - return ( -
- {path}: - {formatValuePreview(value)} -
- ); - } - - const entries = Object.entries(value as object); +const TreeNode = (props: TreeNodeProps) => { + const [isExpanded, setIsExpanded] = createSignal(false); + const canExpand = createMemo( + () => props.value !== null && typeof props.value === "object" && !(props.value instanceof Date), + ); + const entryKeys = createMemo(() => (canExpand() ? Object.keys(props.value as object) : [])); + const getEntryValue = (key: string) => + canExpand() ? (props.value as Record)[key] : undefined; return ( -
-
- - {path}: - {!isExpanded && ( - - {value instanceof Date ? formatValuePreview(value) : `{${Object.keys(value).join(', ')}}`} - - )} -
- {isExpanded && ( -
- {entries.map(([key, val]) => ( - + {props.path}: + {formatValuePreview(props.value)} +
+ } + > +
+
+ + {props.path}: + + + {props.value instanceof Date + ? formatValuePreview(props.value) + : `{${Object.keys(props.value as object).join(", ")}}`} + +
- )} -
+ +
+ + {(key) => ( + + )} + +
+
+
+ ); }; -export const DiffValueView = ({ - value, - expanded, - onToggle, - isNegative, -}: { - value: unknown; - expanded: boolean; - onToggle: () => void; - isNegative: boolean; -}) => { - const { value: safeValue, error } = safeGetValue(value); - - if (error) { - return {error}; - } - - const isExpandable = - safeValue !== null && - typeof safeValue === 'object' && - !(safeValue instanceof Promise); - - if (!isExpandable) { - return {formatValuePreview(safeValue)}; - } - - if (Array.isArray(safeValue)) { - return ( -
- - {expanded && ( -
- {safeValue.map((item, index) => ( - - ))} -
- )} - - {({ ClipboardIcon }) => <>{ClipboardIcon}} - -
- ); - } +export const DiffValueView = (props: DiffValueViewProps) => { + const safeResult = createMemo(() => safeGetValue(props.value)); + const safeValue = () => safeResult().value; + const isExpandable = () => + safeValue() !== null && typeof safeValue() === "object" && !(safeValue() instanceof Promise); + const objectKeys = createMemo(() => + isExpandable() && !Array.isArray(safeValue()) ? Object.keys(safeValue() as object) : [], + ); + const getObjectValue = (key: string) => (safeValue() as Record)[key]; - // Handle objects return ( -
- -
- {!expanded ? ( - {formatValuePreview(safeValue)} - ) : ( -
- {Object.entries(safeValue as object).map(([key, val]) => ( - {safeResult().error}} + > + {formatValuePreview(safeValue())}}> + + +
+ {formatValuePreview(safeValue())}} + > +
+ + {(key) => ( + + )} + +
+
+
+ + {({ ClipboardIcon }) => <>{ClipboardIcon}} +
- )} -
- - {({ ClipboardIcon }) => <>{ClipboardIcon}} - -
+ } + > +
+ + +
+ + {(item, index) => ( + + )} + +
+
+ + {({ ClipboardIcon }) => <>{ClipboardIcon}} + +
+ + + ); }; diff --git a/packages/scan/src/web/views/inspector/flash-overlay.ts b/packages/scan/src/web/views/inspector/flash-overlay.ts index 2164e2c6..47643e8c 100644 --- a/packages/scan/src/web/views/inspector/flash-overlay.ts +++ b/packages/scan/src/web/views/inspector/flash-overlay.ts @@ -12,13 +12,13 @@ const trackElementPosition = ( ): (() => void) => { const handleScroll = callback.bind(null, element); - document.addEventListener('scroll', handleScroll, { + document.addEventListener("scroll", handleScroll, { passive: true, capture: true, }); return () => { - document.removeEventListener('scroll', handleScroll, { capture: true }); + document.removeEventListener("scroll", handleScroll, { capture: true }); }; }; @@ -26,20 +26,18 @@ export const flashManager = { activeFlashes: new Map(), create(container: HTMLElement) { - const existingOverlay = container.querySelector( - '.react-scan-flash-overlay', - ); + const existingOverlay = container.querySelector(".react-scan-flash-overlay"); const overlay = existingOverlay instanceof HTMLElement ? existingOverlay : (() => { - const newOverlay = document.createElement('div'); - newOverlay.className = 'react-scan-flash-overlay'; + const newOverlay = document.createElement("div"); + newOverlay.className = "react-scan-flash-overlay"; container.appendChild(newOverlay); const scrollCleanup = trackElementPosition(container, () => { - if (container.querySelector('.react-scan-flash-overlay')) { + if (container.querySelector(".react-scan-flash-overlay")) { this.create(container); } }); @@ -60,12 +58,12 @@ export const flashManager = { } requestAnimationFrame(() => { - overlay.style.transition = 'none'; - overlay.style.opacity = '0.9'; + overlay.style.transition = "none"; + overlay.style.opacity = "0.9"; const timerId = setTimeout(() => { - overlay.style.transition = 'opacity 150ms ease-out'; - overlay.style.opacity = '0'; + overlay.style.transition = "opacity 150ms ease-out"; + overlay.style.opacity = "0"; const cleanupTimer = setTimeout(() => { if (overlay.parentNode) { diff --git a/packages/scan/src/web/views/inspector/header.tsx b/packages/scan/src/web/views/inspector/header.tsx index b1fe9777..8e676f38 100644 --- a/packages/scan/src/web/views/inspector/header.tsx +++ b/packages/scan/src/web/views/inspector/header.tsx @@ -1,48 +1,32 @@ -import { computed, untracked, useSignalEffect } from '@preact/signals'; -import type { Fiber } from 'bippy'; -import { useMemo, useRef, useState } from 'preact/hooks'; -import { Store } from '~core/index'; -import { signalIsSettingsOpen } from '~web/state'; -import { cn, getExtendedDisplayName } from '~web/utils/helpers'; -import { timelineState } from './states'; - -const headerInspectClassName = computed(() => - cn( - 'absolute inset-0 flex items-center gap-x-2', - 'translate-y-0', - 'transition-transform duration-300', - signalIsSettingsOpen.value && '-translate-y-[200%]', - ), -); +import type { Fiber } from "bippy"; +import { Show, createEffect, createMemo, untrack } from "solid-js"; +import { getInspectState } from "../../../core/native-state"; +import { cn, getExtendedDisplayName } from "../../utils/helpers"; +import { getTimelineState } from "./states"; export const HeaderInspect = () => { - const refReRenders = useRef(null); - const refTiming = useRef(null); - const [currentFiber, setCurrentFiber] = useState(null); - - useSignalEffect(() => { - const state = Store.inspectState.value; + let rerendersElement: HTMLSpanElement | undefined; + let timingElement: HTMLSpanElement | undefined; - if (state.kind === 'focused') { - setCurrentFiber(state.fiber); - } + const currentFiber = createMemo(() => { + const inspectState = getInspectState(); + return inspectState.kind === "focused" ? inspectState.fiber : null; }); - useSignalEffect(() => { - const state = timelineState.value; - untracked(() => { - if (Store.inspectState.value.kind !== 'focused') return; - if (!refReRenders.current || !refTiming.current) return; + createEffect(() => { + const state = getTimelineState(); + untrack(() => { + if (getInspectState().kind !== "focused") return; + if (!rerendersElement || !timingElement) return; - const { totalUpdates, currentIndex, updates, isVisible, windowOffset } = - state; + const { totalUpdates, currentIndex, updates, isVisible, windowOffset } = state; const reRenders = Math.max(0, totalUpdates - 1); const headerText = isVisible ? `#${windowOffset + currentIndex} Re-render` : reRenders > 0 ? `×${reRenders}` - : ''; + : ""; let formattedTime: string | undefined; if (reRenders > 0 && currentIndex >= 0 && currentIndex < updates.length) { @@ -50,77 +34,73 @@ export const HeaderInspect = () => { formattedTime = time > 0 ? time < 0.1 - Number.EPSILON - ? '< 0.1ms' + ? "< 0.1ms" : `${Number(time.toFixed(1))}ms` : undefined; } // TODO(Alexis): can be computed signal - refReRenders.current.dataset.text = headerText ? ` • ${headerText}` : ''; - refTiming.current.dataset.text = formattedTime - ? ` • ${formattedTime}` - : ''; + rerendersElement.dataset.text = headerText ? ` • ${headerText}` : ""; + timingElement.dataset.text = formattedTime ? ` • ${formattedTime}` : ""; }); }); - const componentName = useMemo(() => { - if (!currentFiber) return null; - const { name, wrappers, wrapperTypes } = - getExtendedDisplayName(currentFiber); + const componentName = createMemo(() => { + const fiber = currentFiber(); + if (!fiber) return null; + const { name, wrappers, wrapperTypes } = getExtendedDisplayName(fiber); const title = wrappers.length - ? `${wrappers.join('(')}(${name})${')'.repeat(wrappers.length)}` - : (name ?? ''); + ? `${wrappers.join("(")}(${name})${")".repeat(wrappers.length)}` + : (name ?? ""); const firstWrapperType = wrapperTypes[0]; return ( - - {name ?? 'Unknown'} + + {name ?? "Unknown"} - {!!firstWrapperType && ( - <> - - {firstWrapperType.type} - - {firstWrapperType.compiler && ( - - )} - - )} + + {(wrapperType) => ( + <> + + {wrapperType().type} + + + + + + )} + - {wrapperTypes.length > 1 && ( - - ×{wrapperTypes.length - 1} - - )} + 1}> + ×{wrapperTypes.length - 1} + ); - }, [currentFiber]); + }); return ( -
- {componentName} +
+ {componentName()} {/* useless info */} -
+
- +
); diff --git a/packages/scan/src/web/views/inspector/index.tsx b/packages/scan/src/web/views/inspector/index.tsx index b3364249..4f9fc1de 100644 --- a/packages/scan/src/web/views/inspector/index.tsx +++ b/packages/scan/src/web/views/inspector/index.tsx @@ -1,101 +1,75 @@ -import { computed, untracked, useSignalEffect } from '@preact/signals'; -import type { Fiber } from 'bippy'; -import { Component } from 'preact'; -import { useEffect, useRef } from 'preact/hooks'; -import { Store } from '~core/index'; -import { Icon } from '~web/components/icon'; -import { signalIsSettingsOpen, signalWidgetViews } from '~web/state'; -import { cn } from '~web/utils/helpers'; -import { constant } from '~web/utils/preact/constant'; -import { ComponentsTree } from './components-tree'; -import { flashManager } from './flash-overlay'; +import type { Fiber } from "bippy"; +import { ErrorBoundary, Show, createEffect, onCleanup, untrack } from "solid-js"; +import { getInspectState, setInspectState } from "../../../core/native-state"; +import { Icon } from "../../components/icon"; +import { setWidgetView } from "../../state"; +import { cn } from "../../utils/helpers"; +import { ComponentsTree } from "./components-tree"; +import { flashManager } from "./flash-overlay"; import { + globalInspectorState, + getInspectorUpdateVersion, type TimelineUpdate, - inspectorUpdateSignal, timelineActions, -} from './states'; +} from "./states"; import { collectInspectorData, getStateNames, resetTracking, -} from './timeline/utils'; -import { extractMinimalFiberInfo, getCompositeFiberFromElement } from './utils'; -import { WhatChanged } from './what-changed'; - -export const globalInspectorState = { - lastRendered: new Map(), - expandedPaths: new Set(), - cleanup: () => { - globalInspectorState.lastRendered.clear(); - globalInspectorState.expandedPaths.clear(); - flashManager.cleanupAll(); - resetTracking(); - timelineActions.reset(); - }, +} from "../../../core/inspection/change-collection"; +import { extractMinimalFiberInfo, getCompositeFiberFromElement } from "./utils"; +import { WhatChanged } from "./what-changed"; + +const cleanupInspectorState = () => { + globalInspectorState.lastRendered.clear(); + globalInspectorState.expandedPaths.clear(); + flashManager.cleanupAll(); + resetTracking(); + timelineActions.reset(); }; -// todo: add reset button and error message -class InspectorErrorBoundary extends Component { - state: { error: Error | null; hasError: boolean } = { - hasError: false, - error: null, +const InspectorErrorFallback = (error: Error, reset: () => void) => { + const handleReset = () => { + cleanupInspectorState(); + reset(); }; - static getDerivedStateFromError(e: Error) { - return { hasError: true, error: e }; - } - - handleReset = () => { - this.setState({ hasError: false, error: null }); - globalInspectorState.cleanup(); - }; - - render() { - if (this.state.hasError) { - return ( -
-
- - Something went wrong in the inspector -
-
- {this.state.error?.message || JSON.stringify(this.state.error)} -
- -
- ); - } + return ( +
+
+ + Something went wrong in the inspector +
+
+ {error.message || JSON.stringify(error)} +
+ +
+ ); +}; - return this.props.children; - } -} - -const inspectorContainerClassName = computed(() => - cn( - 'react-scan-inspector', - 'flex-1', - 'opacity-0', - 'overflow-y-auto overflow-x-hidden', - 'transition-opacity delay-0', - 'pointer-events-none', - !signalIsSettingsOpen.value && 'opacity-100 delay-300 pointer-events-auto', - ), +const inspectorContainerClassName = cn( + "react-scan-inspector", + "flex-1", + "opacity-100", + "overflow-y-auto overflow-x-hidden", + "transition-opacity delay-300", + "pointer-events-auto", ); -const Inspector = /* @__PURE__ */ constant(() => { - const refLastInspectedFiber = useRef(null); +const Inspector = () => { + let lastInspectedFiber: Fiber | null = null; - // NOTE(Alexis): no need for useCallback const processUpdate = (fiber: Fiber) => { if (!fiber) return; - refLastInspectedFiber.current = fiber; + lastInspectedFiber = fiber; const { data: inspectorData, shouldUpdate } = collectInspectorData(fiber); if (shouldUpdate) { @@ -112,53 +86,47 @@ const Inspector = /* @__PURE__ */ constant(() => { } }; - useSignalEffect(() => { - const state = Store.inspectState.value; - untracked(() => { - if (state.kind !== 'focused' || !state.focusedDomElement) { - refLastInspectedFiber.current = null; - globalInspectorState.cleanup(); + createEffect(() => { + const state = getInspectState(); + untrack(() => { + if (state.kind !== "focused" || !state.focusedDomElement) { + lastInspectedFiber = null; + cleanupInspectorState(); return; } - if (state.kind === 'focused') { - signalIsSettingsOpen.value = false; - } - const { parentCompositeFiber } = getCompositeFiberFromElement( state.focusedDomElement, state.fiber, ); if (!parentCompositeFiber) { - Store.inspectState.value = { - kind: 'inspect-off', - }; - signalWidgetViews.value = { - view: 'none', - }; + setInspectState({ + kind: "inspect-off", + }); + setWidgetView({ + view: "none", + }); return; } - const isNewComponent = - refLastInspectedFiber.current?.type !== parentCompositeFiber.type; + const isNewComponent = lastInspectedFiber?.type !== parentCompositeFiber.type; if (isNewComponent) { - refLastInspectedFiber.current = parentCompositeFiber; - globalInspectorState.cleanup(); + lastInspectedFiber = parentCompositeFiber; + cleanupInspectorState(); processUpdate(parentCompositeFiber); } }); }); - useSignalEffect(() => { - // NOTE(Alexis): just track - inspectorUpdateSignal.value; - untracked(() => { - const inspectState = Store.inspectState.value; - if (inspectState.kind !== 'focused' || !inspectState.focusedDomElement) { - refLastInspectedFiber.current = null; - globalInspectorState.cleanup(); + createEffect(() => { + getInspectorUpdateVersion(); + untrack(() => { + const inspectState = getInspectState(); + if (inspectState.kind !== "focused" || !inspectState.focusedDomElement) { + lastInspectedFiber = null; + cleanupInspectorState(); return; } @@ -168,51 +136,50 @@ const Inspector = /* @__PURE__ */ constant(() => { ); if (!parentCompositeFiber) { - Store.inspectState.value = { - kind: 'inspect-off', - }; - signalWidgetViews.value = { - view: 'none', - }; + setInspectState({ + kind: "inspect-off", + }); + setWidgetView({ + view: "none", + }); return; } processUpdate(parentCompositeFiber); if (!inspectState.focusedDomElement.isConnected) { - refLastInspectedFiber.current = null; - globalInspectorState.cleanup(); - Store.inspectState.value = { - kind: 'inspecting', + lastInspectedFiber = null; + cleanupInspectorState(); + setInspectState({ + kind: "inspecting", hoveredDomElement: null, - }; + }); } }); }); - useEffect(() => { - return () => { - globalInspectorState.cleanup(); - }; - }, []); + onCleanup(() => { + cleanupInspectorState(); + }); return ( - -
-
+ +
+
- +
); -}); +}; -export const ViewInspector = /* @__PURE__ */ constant(() => { - if (Store.inspectState.value.kind !== 'focused') return null; +export const ViewInspector = () => { return ( - - - - + + + + + + ); -}); +}; diff --git a/packages/scan/src/web/views/inspector/overlay/index.tsx b/packages/scan/src/web/views/inspector/overlay/index.tsx index ee354ed3..dd49721a 100644 --- a/packages/scan/src/web/views/inspector/overlay/index.tsx +++ b/packages/scan/src/web/views/inspector/overlay/index.tsx @@ -1,13 +1,18 @@ import { type Fiber, getDisplayName } from "bippy"; -import { useEffect, useRef } from "preact/hooks"; -import { ReactScanInternals, Store } from "~core/index"; - -import { signalIsSettingsOpen, signalWidgetViews } from "~web/state"; -import { IS_CLIENT } from "~web/utils/constants"; -import { cn, throttle } from "~web/utils/helpers"; +import { createEffect, on, onCleanup, onMount } from "solid-js"; +import { + getInspectState, + getLastReportTime, + getOptionsState, + setInspectState, +} from "../../../../core/native-state"; +import type { States } from "../../../../core/inspection/types"; +import { IS_CLIENT } from "../../../../utils/is-client"; + +import { setWidgetView } from "../../../state"; +import { cn, throttle } from "../../../utils/helpers"; const lerp = (start: number, end: number, t: number) => start + (end - start) * t; import { - type States, findComponentDOMNode, getAssociatedFiberRect, getCompositeComponentFromElement, @@ -42,16 +47,16 @@ const ANIMATION_CONFIG = { const OVERLAY_DPR = IS_CLIENT ? /* @__PURE__ */ window.devicePixelRatio || 1 : 1; export const ScanOverlay = () => { - const refCanvas = useRef(null); - const refEventCatcher = useRef(null); - const refCurrentRect = useRef(null); - const refCurrentLockIconRect = useRef(null); - const refLastHoveredElement = useRef(null); - const refRafId = useRef(0); - const refTimeout = useRef(); - const refCleanupMap = useRef(new Map void>()); - const refIsFadingOut = useRef(false); - const refLastFrameTime = useRef(0); + let canvasElement: HTMLCanvasElement | undefined; + let eventCatcherElement: HTMLDivElement | undefined; + let currentRect: Rect | null = null; + let currentLockIconRect: LockIconRect | null = null; + let lastHoveredElement: Element | null = null; + let animationFrameId = 0; + let timeout: TTimer | undefined; + const cleanupMap = new Map void>(); + let isFadingOut = false; + let lastFrameTime = 0; const drawLockIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => { ctx.save(); @@ -117,14 +122,14 @@ export const ScanOverlay = () => { const lockX = pillX + pillPadding; const lockY = pillY + (pillHeight - lockIconSize) / 2 + 2; drawLockIcon(ctx, lockX, lockY, lockIconSize); - refCurrentLockIconRect.current = { + currentLockIconRect = { x: lockX, y: lockY, width: lockIconSize, height: lockIconSize, }; } else { - refCurrentLockIconRect.current = null; + currentLockIconRect = null; } ctx.fillStyle = "white"; @@ -140,8 +145,8 @@ export const ScanOverlay = () => { kind: DrawKind, fiber: Fiber | null, ) => { - if (!refCurrentRect.current) return; - const rect = refCurrentRect.current; + if (!currentRect) return; + const rect = currentRect; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = "rgba(142, 97, 227, 0.5)"; @@ -168,56 +173,55 @@ export const ScanOverlay = () => { parentCompositeFiber: Fiber, onComplete?: () => void, ) => { - const speed = ReactScanInternals.options.value - .animationSpeed as keyof typeof ANIMATION_CONFIG.speeds; + const speed = getOptionsState().animationSpeed as keyof typeof ANIMATION_CONFIG.speeds; const t = ANIMATION_CONFIG.speeds[speed] ?? ANIMATION_CONFIG.speeds.off; const animationFrame = (timestamp: number) => { - if (timestamp - refLastFrameTime.current < ANIMATION_CONFIG.frameInterval) { - refRafId.current = requestAnimationFrame(animationFrame); + if (timestamp - lastFrameTime < ANIMATION_CONFIG.frameInterval) { + animationFrameId = requestAnimationFrame(animationFrame); return; } - refLastFrameTime.current = timestamp; + lastFrameTime = timestamp; - if (!refCurrentRect.current) { - cancelAnimationFrame(refRafId.current); + if (!currentRect) { + cancelAnimationFrame(animationFrameId); return; } - refCurrentRect.current = { - left: lerp(refCurrentRect.current.left, targetRect.left, t), - top: lerp(refCurrentRect.current.top, targetRect.top, t), - width: lerp(refCurrentRect.current.width, targetRect.width, t), - height: lerp(refCurrentRect.current.height, targetRect.height, t), + currentRect = { + left: lerp(currentRect.left, targetRect.left, t), + top: lerp(currentRect.top, targetRect.top, t), + width: lerp(currentRect.width, targetRect.width, t), + height: lerp(currentRect.height, targetRect.height, t), }; drawRect(canvas, ctx, kind, parentCompositeFiber); const stillMoving = - Math.abs(refCurrentRect.current.left - targetRect.left) > 0.1 || - Math.abs(refCurrentRect.current.top - targetRect.top) > 0.1 || - Math.abs(refCurrentRect.current.width - targetRect.width) > 0.1 || - Math.abs(refCurrentRect.current.height - targetRect.height) > 0.1; + Math.abs(currentRect.left - targetRect.left) > 0.1 || + Math.abs(currentRect.top - targetRect.top) > 0.1 || + Math.abs(currentRect.width - targetRect.width) > 0.1 || + Math.abs(currentRect.height - targetRect.height) > 0.1; if (stillMoving) { - refRafId.current = requestAnimationFrame(animationFrame); + animationFrameId = requestAnimationFrame(animationFrame); } else { - refCurrentRect.current = targetRect; + currentRect = targetRect; drawRect(canvas, ctx, kind, parentCompositeFiber); - cancelAnimationFrame(refRafId.current); + cancelAnimationFrame(animationFrameId); ctx.restore(); onComplete?.(); } }; - cancelAnimationFrame(refRafId.current); - clearTimeout(refTimeout.current); + cancelAnimationFrame(animationFrameId); + clearTimeout(timeout); - refRafId.current = requestAnimationFrame(animationFrame); + animationFrameId = requestAnimationFrame(animationFrame); - refTimeout.current = setTimeout(() => { - cancelAnimationFrame(refRafId.current); - refCurrentRect.current = targetRect; + timeout = setTimeout(() => { + cancelAnimationFrame(animationFrameId); + currentRect = targetRect; drawRect(canvas, ctx, kind, parentCompositeFiber); ctx.restore(); onComplete?.(); @@ -233,8 +237,8 @@ export const ScanOverlay = () => { ) => { ctx.save(); - if (!refCurrentRect.current) { - refCurrentRect.current = targetRect; + if (!currentRect) { + currentRect = targetRect; drawRect(canvas, ctx, kind, parentCompositeFiber); ctx.restore(); return; @@ -260,7 +264,7 @@ export const ScanOverlay = () => { }; const unsubscribeAll = () => { - for (const cleanup of refCleanupMap.current.values()) { + for (const cleanup of cleanupMap.values()) { cleanup?.(); } }; @@ -270,55 +274,55 @@ export const ScanOverlay = () => { if (ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height); } - refCurrentRect.current = null; - refCurrentLockIconRect.current = null; - refLastHoveredElement.current = null; + currentRect = null; + currentLockIconRect = null; + lastHoveredElement = null; canvas.classList.remove("fade-in"); - refIsFadingOut.current = false; + isFadingOut = false; }; const startFadeOut = (onComplete?: () => void) => { - if (!refCanvas.current || refIsFadingOut.current) return; + if (!canvasElement || isFadingOut) return; const handleTransitionEnd = (e: TransitionEvent) => { - if (!refCanvas.current || e.propertyName !== "opacity" || !refIsFadingOut.current) { + if (!canvasElement || e.propertyName !== "opacity" || !isFadingOut) { return; } - refCanvas.current.removeEventListener("transitionend", handleTransitionEnd); - cleanupCanvas(refCanvas.current); + canvasElement.removeEventListener("transitionend", handleTransitionEnd); + cleanupCanvas(canvasElement); onComplete?.(); }; - const existingListener = refCleanupMap.current.get("fade-out"); + const existingListener = cleanupMap.get("fade-out"); if (existingListener) { existingListener(); - refCleanupMap.current.delete("fade-out"); + cleanupMap.delete("fade-out"); } - refCanvas.current.addEventListener("transitionend", handleTransitionEnd); - refCleanupMap.current.set("fade-out", () => { - refCanvas.current?.removeEventListener("transitionend", handleTransitionEnd); + canvasElement.addEventListener("transitionend", handleTransitionEnd); + cleanupMap.set("fade-out", () => { + canvasElement?.removeEventListener("transitionend", handleTransitionEnd); }); - refIsFadingOut.current = true; - refCanvas.current.classList.remove("fade-in"); + isFadingOut = true; + canvasElement.classList.remove("fade-in"); requestAnimationFrame(() => { - refCanvas.current?.classList.add("fade-out"); + canvasElement?.classList.add("fade-out"); }); }; const startFadeIn = () => { - if (!refCanvas.current) return; - refIsFadingOut.current = false; - refCanvas.current.classList.remove("fade-out"); + if (!canvasElement) return; + isFadingOut = false; + canvasElement.classList.remove("fade-out"); requestAnimationFrame(() => { - refCanvas.current?.classList.add("fade-in"); + canvasElement?.classList.add("fade-in"); }); }; const handleHoverableElement = (componentElement: Element) => { - if (componentElement === refLastHoveredElement.current) return; + if (componentElement === lastHoveredElement) return; - refLastHoveredElement.current = componentElement; + lastHoveredElement = componentElement; if (nonVisualTags.has(componentElement.tagName)) { startFadeOut(); @@ -326,14 +330,14 @@ export const ScanOverlay = () => { startFadeIn(); } - Store.inspectState.value = { + setInspectState({ kind: "inspecting", hoveredDomElement: componentElement, - }; + }); }; const handleNonHoverableArea = () => { - if (!refCurrentRect.current || !refCanvas.current || refIsFadingOut.current) { + if (!currentRect || !canvasElement || isFadingOut) { return; } @@ -341,17 +345,17 @@ export const ScanOverlay = () => { }; const handlePointerMove = throttle((e?: PointerEvent) => { - const state = Store.inspectState.peek(); - if (state.kind !== "inspecting" || !refEventCatcher.current) return; + const state = getInspectState(); + if (state.kind !== "inspecting" || !eventCatcherElement) return; - refEventCatcher.current.style.pointerEvents = "none"; + eventCatcherElement.style.pointerEvents = "none"; const element = document.elementFromPoint(e?.clientX ?? 0, e?.clientY ?? 0); - refEventCatcher.current.style.removeProperty("pointer-events"); + eventCatcherElement.style.removeProperty("pointer-events"); - clearTimeout(refTimeout.current); + clearTimeout(timeout); - if (element && element !== refCanvas.current) { + if (element && element !== canvasElement) { const { parentCompositeFiber } = getCompositeComponentFromElement(element as Element); if (parentCompositeFiber) { const componentElement = findComponentDOMNode(parentCompositeFiber); @@ -366,8 +370,8 @@ export const ScanOverlay = () => { }, 32); const isClickInLockIcon = (e: MouseEvent, canvas: HTMLCanvasElement) => { - const currentRect = refCurrentLockIconRect.current; - if (!currentRect) return false; + const lockIconRect = currentLockIconRect; + if (!lockIconRect) return false; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; @@ -378,19 +382,19 @@ export const ScanOverlay = () => { const adjustedY = y / OVERLAY_DPR; return ( - adjustedX >= currentRect.x && - adjustedX <= currentRect.x + currentRect.width && - adjustedY >= currentRect.y && - adjustedY <= currentRect.y + currentRect.height + adjustedX >= lockIconRect.x && + adjustedX <= lockIconRect.x + lockIconRect.width && + adjustedY >= lockIconRect.y && + adjustedY <= lockIconRect.y + lockIconRect.height ); }; const handleLockIconClick = (state: States) => { if (state.kind === "focused") { - Store.inspectState.value = { + setInspectState({ kind: "inspecting", hoveredDomElement: state.focusedDomElement, - }; + }); } }; @@ -401,7 +405,7 @@ export const ScanOverlay = () => { return; } - const tagName = refLastHoveredElement.current?.tagName; + const tagName = lastHoveredElement?.tagName; if (tagName && nonVisualTags.has(tagName)) { return; } @@ -409,16 +413,15 @@ export const ScanOverlay = () => { e.preventDefault(); e.stopPropagation(); - const element = - refLastHoveredElement.current ?? document.elementFromPoint(e.clientX, e.clientY); + const element = lastHoveredElement ?? document.elementFromPoint(e.clientX, e.clientY); if (!element) return; const clickedEl = e.composedPath().at(0); if (clickedEl instanceof HTMLElement && clickableElements.includes(clickedEl.id)) { - const syntheticEvent = new MouseEvent(e.type, e); - // @ts-ignore - this allows to know to not re-process this event when this event handler captures it - syntheticEvent.__reactScanSyntheticEvent = true; + const syntheticEvent = Object.assign(new MouseEvent(e.type, e), { + __reactScanSyntheticEvent: true, + }); clickedEl.dispatchEvent(syntheticEvent); return; } @@ -428,29 +431,28 @@ export const ScanOverlay = () => { const componentElement = findComponentDOMNode(parentCompositeFiber); if (!componentElement) { - refLastHoveredElement.current = null; - Store.inspectState.value = { + lastHoveredElement = null; + setInspectState({ kind: "inspect-off", - }; + }); return; } - Store.inspectState.value = { + setInspectState({ kind: "focused", focusedDomElement: componentElement, fiber: parentCompositeFiber, - }; + }); }; const handleClick = (e: MouseEvent) => { - // @ts-ignore - metadata added to toolbar button events we create and dispatch - if (e.__reactScanSyntheticEvent) { + if ("__reactScanSyntheticEvent" in e && e.__reactScanSyntheticEvent === true) { return; } - const state = Store.inspectState.peek(); - const canvas = refCanvas.current; - if (!canvas || !refEventCatcher.current) return; + const state = getInspectState(); + const canvas = canvasElement; + if (!canvas || !eventCatcherElement) return; if (isClickInLockIcon(e, canvas)) { e.preventDefault(); @@ -467,17 +469,17 @@ export const ScanOverlay = () => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key !== "Escape") return; - const state = Store.inspectState.peek(); - const canvas = refCanvas.current; + const state = getInspectState(); + const canvas = canvasElement; if (!canvas) return; if (document.activeElement?.id === "react-scan-root") { return; } - signalWidgetViews.value = { + setWidgetView({ view: "none", - }; + }); if (state.kind === "focused" || state.kind === "inspecting") { e.preventDefault(); @@ -486,20 +488,19 @@ export const ScanOverlay = () => { switch (state.kind) { case "focused": { startFadeIn(); - refCurrentRect.current = null; - refLastHoveredElement.current = state.focusedDomElement; - Store.inspectState.value = { + currentRect = null; + lastHoveredElement = state.focusedDomElement; + setInspectState({ kind: "inspecting", hoveredDomElement: state.focusedDomElement, - }; + }); break; } case "inspecting": { startFadeOut(() => { - signalIsSettingsOpen.value = false; - Store.inspectState.value = { + setInspectState({ kind: "inspect-off", - }; + }); }); break; } @@ -512,20 +513,18 @@ export const ScanOverlay = () => { canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, ) => { - refCleanupMap.current.get(state.kind)?.(); + cleanupMap.get(state.kind)?.(); - if (refEventCatcher.current) { + if (eventCatcherElement) { if (state.kind !== "inspecting") { - refEventCatcher.current.style.pointerEvents = "none"; + eventCatcherElement.style.pointerEvents = "none"; } } - if (refRafId.current) { - cancelAnimationFrame(refRafId.current); + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); } - let unsubReport: (() => void) | undefined; - switch (state.kind) { case "inspect-off": startFadeOut(); @@ -538,30 +537,15 @@ export const ScanOverlay = () => { case "focused": if (!state.focusedDomElement) return; - if (refLastHoveredElement.current !== state.focusedDomElement) { - refLastHoveredElement.current = state.focusedDomElement; + if (lastHoveredElement !== state.focusedDomElement) { + lastHoveredElement = state.focusedDomElement; } - signalWidgetViews.value = { + setWidgetView({ view: "inspector", - }; - - drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); - - unsubReport = Store.lastReportTime.subscribe(() => { - if (refRafId.current && refCurrentRect.current) { - const { parentCompositeFiber } = getCompositeComponentFromElement( - state.focusedDomElement, - ); - if (parentCompositeFiber) { - drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); - } - } }); - if (unsubReport) { - refCleanupMap.current.set(state.kind, unsubReport); - } + drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); break; } }; @@ -575,17 +559,17 @@ export const ScanOverlay = () => { }; const handleResizeOrScroll = () => { - const state = Store.inspectState.peek(); - const canvas = refCanvas.current; + const state = getInspectState(); + const canvas = canvasElement; if (!canvas) return; const ctx = canvas?.getContext("2d"); if (!ctx) return; - cancelAnimationFrame(refRafId.current); - clearTimeout(refTimeout.current); + cancelAnimationFrame(animationFrameId); + clearTimeout(timeout); updateCanvasSize(canvas, ctx); - refCurrentRect.current = null; + currentRect = null; if (state.kind === "focused" && state.focusedDomElement) { drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); @@ -595,8 +579,8 @@ export const ScanOverlay = () => { }; const handlePointerDown = (e: PointerEvent) => { - const state = Store.inspectState.peek(); - const canvas = refCanvas.current; + const state = getInspectState(); + const canvas = canvasElement; if (!canvas) return; if (state.kind === "inspecting" || isClickInLockIcon(e as unknown as MouseEvent, canvas)) { @@ -606,18 +590,40 @@ export const ScanOverlay = () => { } }; - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - const canvas = refCanvas.current; + onMount(() => { + const canvas = canvasElement; if (!canvas) return; const ctx = canvas?.getContext("2d"); if (!ctx) return; updateCanvasSize(canvas, ctx); - const unSubState = Store.inspectState.subscribe((state) => { - handleStateChange(state, canvas, ctx); - }); + createEffect( + on( + getInspectState, + (state) => { + handleStateChange(state, canvas, ctx); + }, + { defer: true }, + ), + ); + + createEffect( + on( + getLastReportTime, + () => { + const state = getInspectState(); + if (state.kind !== "focused" || !animationFrameId || !currentRect) return; + const { parentCompositeFiber } = getCompositeComponentFromElement( + state.focusedDomElement, + ); + if (parentCompositeFiber) { + drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); + } + }, + { defer: true }, + ), + ); window.addEventListener("scroll", handleResizeOrScroll, { passive: true }); window.addEventListener("resize", handleResizeOrScroll, { passive: true }); @@ -631,9 +637,8 @@ export const ScanOverlay = () => { document.addEventListener("click", handleClick, { capture: true }); document.addEventListener("keydown", handleKeyDown, { capture: true }); - return () => { + onCleanup(() => { unsubscribeAll(); - unSubState(); window.removeEventListener("scroll", handleResizeOrScroll); window.removeEventListener("resize", handleResizeOrScroll); document.removeEventListener("pointermove", handlePointerMove, { @@ -645,27 +650,27 @@ export const ScanOverlay = () => { }); document.removeEventListener("keydown", handleKeyDown, { capture: true }); - if (refRafId.current) { - cancelAnimationFrame(refRafId.current); + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); } - clearTimeout(refTimeout.current); - }; - }, []); + clearTimeout(timeout); + }); + }); return ( <>
| string; + type: Fiber["type"]; displayName: string; selfTime: number; totalTime: number; @@ -35,6 +34,11 @@ export interface TimelineState { export const TIMELINE_MAX_UPDATES = 1000; +export const globalInspectorState = { + lastRendered: new Map(), + expandedPaths: new Set(), +}; + const timelineStateDefault: TimelineState = { updates: [], currentFiber: null, @@ -47,9 +51,16 @@ const timelineStateDefault: TimelineState = { playbackSpeed: 1, }; -export const timelineState = signal(timelineStateDefault); +const [getTimelineState, setTimelineState] = + /* @__PURE__ */ createSignal(timelineStateDefault); + +const [getInspectorUpdateVersion, setInspectorUpdateVersion] = /* @__PURE__ */ createSignal(0); -export const inspectorUpdateSignal = signal(0); +export { getInspectorUpdateVersion, getTimelineState, setTimelineState }; + +export const notifyInspectorUpdate = () => { + setInspectorUpdateVersion((version) => version + 1); +}; let pendingUpdates: Array<{ update: TimelineUpdate; fiber: Fiber | null }> = []; let batchTimeout: ReturnType | null = null; @@ -59,7 +70,7 @@ const batchUpdates = () => { const batchedUpdates = [...pendingUpdates]; - const { updates, totalUpdates, currentIndex, isViewingHistory } = timelineState.value; + const { updates, totalUpdates, currentIndex, isViewingHistory } = getTimelineState(); const newUpdates = [...updates]; let newTotalUpdates = totalUpdates; @@ -92,15 +103,15 @@ const batchUpdates = () => { const lastUpdate = batchedUpdates[batchedUpdates.length - 1]; - timelineState.value = { - ...timelineState.value, + setTimelineState({ + ...getTimelineState(), latestFiber: lastUpdate.fiber, updates: newUpdates, totalUpdates: newTotalUpdates, windowOffset: newWindowOffset, currentIndex: newCurrentIndex, isViewingHistory, - }; + }); // Only after signal is updated, remove the processed updates pendingUpdates = pendingUpdates.slice(batchedUpdates.length); @@ -108,33 +119,34 @@ const batchUpdates = () => { export const timelineActions = { showTimeline: () => { - timelineState.value = { - ...timelineState.value, + setTimelineState({ + ...getTimelineState(), isVisible: true, - }; + }); }, hideTimeline: () => { - timelineState.value = { - ...timelineState.value, + const state = getTimelineState(); + setTimelineState({ + ...state, isVisible: false, - currentIndex: timelineState.value.updates.length - 1, - }; + currentIndex: state.updates.length - 1, + }); }, updateFrame: (index: number, isViewingHistory: boolean) => { - timelineState.value = { - ...timelineState.value, + setTimelineState({ + ...getTimelineState(), currentIndex: index, isViewingHistory, - }; + }); }, updatePlaybackSpeed: (speed: TimelineState["playbackSpeed"]) => { - timelineState.value = { - ...timelineState.value, + setTimelineState({ + ...getTimelineState(), playbackSpeed: speed, - }; + }); }, addUpdate: (update: TimelineUpdate, latestFiber: Fiber | null) => { @@ -161,6 +173,6 @@ export const timelineActions = { batchTimeout = null; } pendingUpdates = []; - timelineState.value = timelineStateDefault; + setTimelineState(timelineStateDefault); }, }; diff --git a/packages/scan/src/web/views/inspector/utils.ts b/packages/scan/src/web/views/inspector/utils.ts index 4207bb76..baf3b396 100644 --- a/packages/scan/src/web/views/inspector/utils.ts +++ b/packages/scan/src/web/views/inspector/utils.ts @@ -1,88 +1,19 @@ +import { type Fiber, getDisplayName, getTimings, traverseFiber } from "bippy"; +import { ReactScanInternals } from "../../../core/index"; import { - type Fiber, - FunctionComponentTag, - type MemoizedState, - getDisplayName, - getTimings, - isCompositeFiber, - isHostFiber, - traverseFiber, -} from "bippy"; -import { type PropsChange, ReactScanInternals } from "~core/index"; -import { ChangeReason } from "~core/instrumentation"; -import { isEqual } from "~core/utils"; -import { globalInspectorState } from "."; -import type { ExtendedReactRenderer } from "../../../types"; -import { TIMELINE_MAX_UPDATES } from "./states"; + getFiberFromElement, + getParentCompositeFiber, + isPromise, +} from "../../../core/inspection/fiber"; import type { MinimalFiberInfo } from "./states"; -import { getAllFiberContexts, getStateNames } from "./timeline/utils"; -interface StateItem { - name: string; - value: unknown; -} - -// todo, change this to currently focused fiber -export type States = - | { - kind: "inspecting"; - hoveredDomElement: Element | null; - } - | { - kind: "inspect-off"; - } - | { - kind: "focused"; - focusedDomElement: Element; - fiber: Fiber; - } - | { - kind: "uninitialized"; - }; - -interface ReactRootContainer { - _reactRootContainer?: { - _internalRoot?: { - current?: { - child: Fiber; - }; - }; - }; -} - -interface ReactInternalProps { - [key: string]: Fiber; -} - -export const getFiberFromElement = (element: Element): Fiber | null => { - if ("__REACT_DEVTOOLS_GLOBAL_HOOK__" in window) { - const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; - if (!hook?.renderers) return null; - - for (const [, renderer] of Array.from(hook.renderers)) { - try { - const fiber = renderer.findFiberByHostInstance?.(element); - if (fiber) return fiber; - } catch { - // If React is mid-render, references to previous nodes may disappear - } - } - } - - if ("_reactRootContainer" in element) { - const elementWithRoot = element as unknown as ReactRootContainer; - const rootContainer = elementWithRoot._reactRootContainer; - return rootContainer?._internalRoot?.current?.child ?? null; - } - - for (const key in element) { - if (key.startsWith("__reactInternalInstance$") || key.startsWith("__reactFiber")) { - const elementWithFiber = element as unknown as ReactInternalProps; - return elementWithFiber[key]; - } - } - return null; -}; +export { + getChangedPropsDetailed, + getFiberFromElement, + getParentCompositeFiber, + isPromise, +} from "../../../core/inspection/fiber"; +export type { States } from "../../../core/inspection/types"; const getFirstStateNode = (fiber: Fiber): Element | null => { let current: Fiber | null = fiber; @@ -124,19 +55,6 @@ const getNearestFiberFromElement = (element: Element | null): Fiber | null => { } }; -export const getParentCompositeFiber = (fiber: Fiber): readonly [Fiber, Fiber | null] | null => { - let current: Fiber | null = fiber; - let prevHost: Fiber | null = null; - - while (current) { - if (isCompositeFiber(current)) return [current, prevHost] as const; - if (isHostFiber(current) && !prevHost) prevHost = current; - current = current.return; - } - - return null; -}; - const isFiberInTree = (fiber: Fiber, root: Fiber): boolean => { { // const root= fiberRootCache.get(fiber) || (fiber.alternate && fiberRootCache.get(fiber.alternate) ) @@ -149,39 +67,6 @@ const isFiberInTree = (fiber: Fiber, root: Fiber): boolean => { } }; -const isCurrentTree = (fiber: Fiber) => { - let curr: Fiber | null = fiber; - let rootFiber: Fiber | null = null; - - while (curr) { - // todo: make sure removing null check doesn't break - // todo: document that fiber stores root in stateNode - if (!curr.stateNode) { - curr = curr.return; - continue; - } - // if the app never rendered then fiber roots will always return false, but thats fine since we don't care which - // fiber we read from when there never has been a re-render - // todo: document that better - if (ReactScanInternals.instrumentation?.fiberRoots.has(curr.stateNode)) { - rootFiber = curr; - - break; - } - - curr = curr.return; - } - - if (!rootFiber) { - return false; - } - - const fiberRoot = rootFiber.stateNode; - const currentRootFiber = fiberRoot.current; - - return isFiberInTree(fiber, currentRootFiber); -}; - export const getAssociatedFiberRect = async (element: Element) => { const associatedFiber = getNearestFiberFromElement(element); @@ -262,128 +147,6 @@ export const getCompositeFiberFromElement = (element: Element, knownFiber?: Fibe }; }; -export const getChangedPropsDetailed = (fiber: Fiber): Array => { - const currentProps = fiber.memoizedProps ?? {}; - const previousProps = fiber.alternate?.memoizedProps ?? {}; - const changes: Array = []; - - for (const key in currentProps) { - if (key === "children") continue; - - const currentValue = currentProps[key]; - const prevValue = previousProps[key]; - - if (!isEqual(currentValue, prevValue)) { - changes.push({ - name: key, - value: currentValue, - prevValue, - type: ChangeReason.Props, - }); - } - } - - return changes; -}; - -export interface OverrideMethods { - overrideProps: ((fiber: Fiber, path: string[], value: unknown) => void) | null; - overrideHookState: ((fiber: Fiber, id: string, path: string[], value: unknown) => void) | null; - overrideContext: ((fiber: Fiber, contextType: unknown, value: unknown) => void) | null; -} - -const isRecord = (value: unknown): value is Record => { - return value !== null && typeof value === "object"; -}; - -const getOverrideMethods = (): OverrideMethods => { - let overrideProps: OverrideMethods["overrideProps"] = null; - let overrideHookState: OverrideMethods["overrideHookState"] = null; - let overrideContext: OverrideMethods["overrideContext"] = null; - - if ("__REACT_DEVTOOLS_GLOBAL_HOOK__" in window) { - const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; - if (!hook?.renderers) { - return { - overrideProps: null, - overrideHookState: null, - overrideContext: null, - }; - } - - for (const [, renderer] of Array.from(hook.renderers)) { - try { - const devToolsRenderer = renderer as ExtendedReactRenderer; - - if (overrideHookState) { - const prevOverrideHookState = overrideHookState; - overrideHookState = (fiber: Fiber, id: string, path: string[], value: unknown) => { - // Find the hook - let current = fiber.memoizedState; - for (let i = 0; i < Number(id); i++) { - if (!current?.next) break; - current = current.next; - } - - if (current?.queue) { - // Update through React's queue mechanism - const queue = current.queue; - if (isRecord(queue) && "dispatch" in queue) { - const dispatch = queue.dispatch as (value: unknown) => void; - dispatch(value); - return; - } - } - - // Chain updates through all renderers to ensure consistency across different React renderers - // (e.g., React DOM + React Native Web in the same app) - prevOverrideHookState(fiber, id, path, value); - devToolsRenderer.overrideHookState?.(fiber, id, path, value); - }; - } else if (devToolsRenderer.overrideHookState) { - overrideHookState = devToolsRenderer.overrideHookState; - } - - if (overrideProps) { - const prevOverrideProps = overrideProps; - overrideProps = (fiber: Fiber, path: Array, value: unknown) => { - // Chain updates through all renderers to maintain consistency - prevOverrideProps(fiber, path, value); - devToolsRenderer.overrideProps?.(fiber, path, value); - }; - } else if (devToolsRenderer.overrideProps) { - overrideProps = devToolsRenderer.overrideProps; - } - - // For context, we don't need the chaining pattern since we're using overrideProps internally - // to update the context provider's value prop, which already handles the chaining - overrideContext = (fiber: Fiber, contextType: unknown, value: unknown) => { - // Find the provider fiber for this context - let current: Fiber | null = fiber; - while (current) { - const type = current.type as { Provider?: unknown }; - if (type === contextType || type?.Provider === contextType) { - // Found the provider, update both current and alternate fibers - if (overrideProps) { - overrideProps(current, ["value"], value); - if (current.alternate) { - overrideProps(current.alternate, ["value"], value); - } - } - break; - } - current = current.return; - } - }; - } catch { - /**/ - } - } - } - - return { overrideProps, overrideHookState, overrideContext }; -}; - export const nonVisualTags = new Set([ "HTML", "HEAD", @@ -415,11 +178,7 @@ export const findComponentDOMNode = ( ): HTMLElement | null => { if (fiber.stateNode && "nodeType" in fiber.stateNode) { const element = fiber.stateNode as HTMLElement; - if ( - excludeNonVisualTags && - element.tagName && - nonVisualTags.has(element.tagName.toLowerCase()) - ) { + if (excludeNonVisualTags && element.tagName && nonVisualTags.has(element.tagName)) { return null; } return element; @@ -482,46 +241,6 @@ export const getInspectableElements = ( return result; }; -const fiberMap = new WeakMap(); - -const getInspectableAncestors = (element: HTMLElement): Array => { - const result: Array = []; - - const findInspectableFiber = (element: HTMLElement | null): HTMLElement | null => { - if (!element) return null; - const { parentCompositeFiber } = getCompositeComponentFromElement(element); - if (!parentCompositeFiber) return null; - - const componentRoot = findComponentDOMNode(parentCompositeFiber); - if (componentRoot === element) { - // Store the fiber reference in WeakMap - fiberMap.set(element, parentCompositeFiber); - return element; - } - return null; - }; - - let current: HTMLElement | null = element; - while (current && current !== document.body) { - const inspectable = findInspectableFiber(current); - if (inspectable) { - // Get fiber from WeakMap - const fiber = fiberMap.get(inspectable); - if (fiber) { - result.unshift({ - element: inspectable, - depth: 0, - name: getDisplayName(fiber.type) ?? "Unknown", - fiber, - }); - } - } - current = current.parentElement; - } - - return result; -}; - type DiffResult = { type: "primitive" | "reference" | "object"; changes: Array<{ @@ -540,25 +259,6 @@ type DiffChange = { sameFunction?: boolean; }; -type InspectableValue = - | Record - | Array - | Map - | Set - | ArrayBuffer - | DataView - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array - | BigInt64Array - | BigUint64Array; - export type AggregatedChanges = { count: number; // unstable: boolean; @@ -568,113 +268,6 @@ export type AggregatedChanges = { name: string; }; -const isExpandable = (value: unknown): value is InspectableValue => { - if (value === null || typeof value !== "object" || isPromise(value)) { - return false; - } - - if (value instanceof ArrayBuffer) { - return true; - } - - if (value instanceof DataView) { - return true; - } - - if (ArrayBuffer.isView(value)) { - return true; - } - - if (value instanceof Map || value instanceof Set) { - return value.size > 0; - } - - if (Array.isArray(value)) { - return value.length > 0; - } - - return Object.keys(value).length > 0; -}; - -const isEditableValue = (value: unknown, parentPath?: string): boolean => { - if (value == null) return true; - - if (isPromise(value)) return false; - - if (typeof value === "function") { - return false; - } - - if (parentPath) { - const parts = parentPath.split("."); - let currentPath = ""; - for (const part of parts) { - currentPath = currentPath ? `${currentPath}.${part}` : part; - const obj = globalInspectorState.lastRendered.get(currentPath); - if (obj instanceof DataView || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) { - return false; - } - } - } - - switch (value.constructor) { - case Date: - case RegExp: - case Error: - return true; - default: - switch (typeof value) { - case "string": - case "number": - case "boolean": - case "bigint": - return true; - default: - return false; - } - } -}; - -const getPath = ( - componentName: string, - section: string, - parentPath: string, - key: string, -): string => { - if (parentPath) { - return `${componentName}.${parentPath}.${key}`; - } - - if (section === "context" && !key.startsWith("context.")) { - return `${componentName}.${section}.context.${key}`; - } - - return `${componentName}.${section}.${key}`; -}; - -const sanitizeString = (value: string): string => { - return value - .replace(/[<>]/g, "") - .replace(/javascript:/gi, "") - .replace(/data:/gi, "") - .replace(/on\w+=/gi, "") - .slice(0, 50000); -}; - -const sanitizeErrorMessage = (error: string): string => { - return error - .replace(/[<>]/g, "") - .replace(/&/g, "&") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/\//g, "/"); -}; - -const formatValue = (value: unknown): string => { - const metadata = ensureRecord(value); - return metadata.displayValue as string; -}; - export const formatForClipboard = (value: unknown): string => { try { if (value === null) return "null"; @@ -860,107 +453,6 @@ const parseValue = (value: string, currentType: unknown): unknown => { } }; -const detectValueType = ( - value: string, -): { - type: "string" | "number" | "undefined" | "null" | "boolean"; - value: unknown; -} => { - const trimmed = value.trim(); - - switch (trimmed) { - case "undefined": - return { type: "undefined", value: undefined }; - case "null": - return { type: "null", value: null }; - case "true": - return { type: "boolean", value: true }; - case "false": - return { type: "boolean", value: false }; - } - - if (/^".*"$/.test(trimmed)) { - return { type: "string", value: trimmed.slice(1, -1) }; - } - - if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) { - return { type: "number", value: Number(trimmed) }; - } - - return { type: "string", value: `"${trimmed}"` }; -}; - -const formatInitialValue = (value: unknown): string => { - if (value === undefined) return "undefined"; - if (value === null) return "null"; - if (typeof value === "string") return `"${value}"`; - return String(value); -}; - -const updateNestedValue = (obj: unknown, path: Array, value: unknown): unknown => { - try { - if (path.length === 0) return value; - - const [key, ...rest] = path; - - // Handle our special array of {name, value} pairs - if ( - Array.isArray(obj) && - obj.every((item): item is StateItem => "name" in item && "value" in item) - ) { - const index = obj.findIndex((item) => item.name === key); - if (index === -1) return obj; - - const newArray = [...obj]; - if (rest.length === 0) { - newArray[index] = { ...newArray[index], value }; - } else { - newArray[index] = { - ...newArray[index], - value: updateNestedValue(newArray[index].value, rest, value), - }; - } - return newArray; - } - - if (obj instanceof Map) { - const newMap = new Map(obj); - if (rest.length === 0) { - newMap.set(key, value); - } else { - const currentValue = newMap.get(key); - newMap.set(key, updateNestedValue(currentValue, rest, value)); - } - return newMap; - } - - if (Array.isArray(obj)) { - const index = Number.parseInt(key, 10); - const newArray = [...obj]; - if (rest.length === 0) { - newArray[index] = value; - } else { - newArray[index] = updateNestedValue(obj[index], rest, value); - } - return newArray; - } - - if (obj && typeof obj === "object") { - if (rest.length === 0) { - return { ...obj, [key]: value }; - } - return { - ...obj, - [key]: updateNestedValue((obj as Record)[key], rest, value), - }; - } - - return value; - } catch { - return obj; - } -}; - const areFunctionsEqual = (prev: unknown, current: unknown): boolean => { try { // Check if both values are actually functions @@ -1080,23 +572,6 @@ export const formatPath = (path: string[]): string => { }, ""); }; -const formatFunctionBody = (body: string): string => { - // Remove newlines and extra spaces - let formatted = body.replace(/\s+/g, " ").trim(); - - // Add newlines after {, ; and before } - formatted = formatted - .replace(/{/g, "{\n ") - .replace(/;/g, ";\n ") - .replace(/}/g, "\n}") - .replace(/{\s+}/g, "{ }"); // Clean up empty blocks - - // Clean up arrow functions - formatted = formatted.replace(/=> {\n/g, "=> {").replace(/\n\s*}\s*$/g, " }"); - - return formatted; -}; - function hackyJsFormatter(code: string) { // // 1) Collapse runs of whitespace to single spaces @@ -1134,7 +609,7 @@ function hackyJsFormatter(code: string) { } // Single/double char punctuation - if (/[(){}[\];,<>:\?!]/.test(c)) { + if (/[(){}[\];,<>:?!]/.test(c)) { // If we had something in current, push it if (current.trim()) { rawTokens.push(current.trim()); @@ -1332,7 +807,7 @@ function hackyJsFormatter(code: string) { } // Combined empty pairs like '()', '[]', '{}', '<>' - else if (/^\(\)|\[\]|\{\}|\<\>$/.test(tok)) { + else if (/^\(\)|\[\]|\{\}|<>$/.test(tok)) { placeToken(tok); // Arrow => @@ -1434,408 +909,6 @@ export const safeGetValue = (value: unknown): { value: unknown; error?: string } } }; -export interface TimelineSliderValues { - leftValue: number; - min: number; - max: number; - value: number; - rightValue: number; -} - -const calculateSliderValues = ( - totalUpdates: number, - currentIndex: number, -): TimelineSliderValues => { - if (totalUpdates <= TIMELINE_MAX_UPDATES) { - return { - leftValue: 0, - min: 0, - max: totalUpdates - 1, - value: currentIndex, - rightValue: totalUpdates - 1, - }; - } - - return { - leftValue: totalUpdates - TIMELINE_MAX_UPDATES, - min: 0, - max: TIMELINE_MAX_UPDATES - 1, - value: currentIndex, - rightValue: totalUpdates - 1, - }; -}; - -// be careful, this is an implementation detail is not stable or reliable across all react versions https://github.com/facebook/react/pull/15124 -// type UpdateQueue = { -// last: Update | null, -// dispatch: (A => mixed) | null, -// eagerReducer: ((S, A) => S) | null, -// eagerState: S | null, -// }; - -interface ExtendedMemoizedState extends MemoizedState { - queue?: { - lastRenderedState: unknown; - } | null; - element?: unknown; -} - -const isDirectComponent = (fiber: Fiber): boolean => { - if (!fiber || !fiber.type) return false; - - const isFunctionalComponent = typeof fiber.type === "function"; - const isClassComponent = fiber.type?.prototype?.isReactComponent ?? false; - - if (!(isFunctionalComponent || isClassComponent)) return false; - - if (isClassComponent) { - return true; - } - - let memoizedState = fiber.memoizedState; - while (memoizedState) { - if (memoizedState.queue) { - return true; - } - const nextState: ExtendedMemoizedState | null = memoizedState.next; - if (!nextState) break; - memoizedState = nextState; - } - - return false; -}; - -export const isPromise = (value: unknown): value is Promise => { - return !!value && (value instanceof Promise || (typeof value === "object" && "then" in value)); -}; - -const ensureRecord = ( - value: unknown, - maxDepth = 2, - seen = new WeakSet(), -): Record => { - if (isPromise(value)) { - return { type: "promise", displayValue: "Promise" }; - } - - if (value === null) { - return { type: "null", displayValue: "null" }; - } - - if (value === undefined) { - return { type: "undefined", displayValue: "undefined" }; - } - - switch (typeof value) { - case "object": { - if (seen.has(value)) { - return { type: "circular", displayValue: "[Circular Reference]" }; - } - - if (!value) return { type: "null", displayValue: "null" }; - - seen.add(value); - - try { - const result: Record = {}; - - if (value instanceof Element) { - result.type = "Element"; - result.tagName = value.tagName.toLowerCase(); - result.displayValue = value.tagName.toLowerCase(); - return result; - } - - if (value instanceof Map) { - result.type = "Map"; - result.size = value.size; - result.displayValue = `Map(${value.size})`; - - if (maxDepth > 0) { - const entries: Record = {}; - let index = 0; - for (const [key, val] of value.entries()) { - if (index >= 50) break; - try { - entries[String(key)] = ensureRecord(val, maxDepth - 1, seen); - } catch { - entries[String(index)] = { - type: "error", - displayValue: "Error accessing Map entry", - }; - } - index++; - } - result.entries = entries; - } - return result; - } - - if (value instanceof Set) { - result.type = "Set"; - result.size = value.size; - result.displayValue = `Set(${value.size})`; - - if (maxDepth > 0) { - const items = []; - let count = 0; - for (const item of value) { - if (count >= 50) break; - items.push(ensureRecord(item, maxDepth - 1, seen)); - count++; - } - result.items = items; - } - return result; - } - - if (value instanceof Date) { - result.type = "Date"; - result.value = value.toISOString(); - result.displayValue = value.toLocaleString(); - return result; - } - - if (value instanceof RegExp) { - result.type = "RegExp"; - result.value = value.toString(); - result.displayValue = value.toString(); - return result; - } - - if (value instanceof Error) { - result.type = "Error"; - result.name = value.name; - result.message = value.message; - result.displayValue = `${value.name}: ${value.message}`; - return result; - } - - if (value instanceof ArrayBuffer) { - result.type = "ArrayBuffer"; - result.byteLength = value.byteLength; - result.displayValue = `ArrayBuffer(${value.byteLength})`; - return result; - } - - if (value instanceof DataView) { - result.type = "DataView"; - result.byteLength = value.byteLength; - result.displayValue = `DataView(${value.byteLength})`; - return result; - } - - if (ArrayBuffer.isView(value)) { - const typedArray = value as unknown as { - length: number; - constructor: { name: string }; - buffer: ArrayBuffer; - }; - result.type = typedArray.constructor.name; - result.length = typedArray.length; - result.byteLength = typedArray.buffer.byteLength; - result.displayValue = `${typedArray.constructor.name}(${typedArray.length})`; - return result; - } - - if (Array.isArray(value)) { - result.type = "array"; - result.length = value.length; - result.displayValue = `Array(${value.length})`; - - if (maxDepth > 0) { - result.items = value.slice(0, 50).map((item) => ensureRecord(item, maxDepth - 1, seen)); - } - return result; - } - - const keys = Object.keys(value); - result.type = "object"; - result.size = keys.length; - result.displayValue = - keys.length <= 5 - ? `{${keys.join(", ")}}` - : `{${keys.slice(0, 5).join(", ")}, ...${keys.length - 5}}`; - - if (maxDepth > 0) { - const entries: Record = {}; - for (const key of keys.slice(0, 50)) { - try { - entries[key] = ensureRecord( - (value as Record)[key], - maxDepth - 1, - seen, - ); - } catch { - entries[key] = { - type: "error", - displayValue: "Error accessing property", - }; - } - } - result.entries = entries; - } - return result; - } finally { - seen.delete(value); - } - } - case "string": - return { - type: "string", - value, - displayValue: `"${value}"`, - }; - case "function": - return { - type: "function", - displayValue: "ƒ()", - name: value.name || "anonymous", - }; - default: - return { - type: typeof value, - value, - displayValue: String(value), - }; - } -}; - -const getCurrentFiberState = (fiber: Fiber): Record | null => { - if (fiber.tag !== FunctionComponentTag || !isDirectComponent(fiber)) { - return null; - } - - const currentIsNewer = fiber.alternate - ? (fiber.actualStartTime ?? 0) > (fiber.alternate.actualStartTime ?? 0) - : true; - - const memoizedState: ExtendedMemoizedState | null = currentIsNewer - ? fiber.memoizedState - : (fiber.alternate?.memoizedState ?? fiber.memoizedState); - - if (!memoizedState) return null; - - return memoizedState; -}; - -const replayComponent = async (fiber: Fiber): Promise => { - const { overrideProps, overrideHookState, overrideContext } = getOverrideMethods(); - if (!overrideProps || !overrideHookState || !fiber) return; - - try { - // Handle props updates - const currentProps = fiber.memoizedProps || {}; - const propKeys = Object.keys(currentProps).filter((key) => { - const value = currentProps[key]; - if (Array.isArray(value) || typeof value === "string") { - return !Number.isInteger(Number(key)) && key !== "length"; - } - return true; - }); - - for (const key of propKeys) { - try { - const value = currentProps[key]; - // For arrays and objects, we need to clone to trigger updates - const propValue = Array.isArray(value) - ? [...value] - : typeof value === "object" && value !== null - ? { ...value } - : value; - overrideProps(fiber, [key], propValue); - } catch {} - } - - // Handle state updates - const currentState = getCurrentFiberState(fiber); - if (currentState) { - const stateNames = getStateNames(fiber); - - // First, handle named state hooks - for (const [key, value] of Object.entries(currentState)) { - try { - const namedStateIndex = stateNames.indexOf(key); - if (namedStateIndex !== -1) { - const hookId = namedStateIndex.toString(); - // For arrays and objects, we need to clone to trigger updates - const stateValue = Array.isArray(value) - ? [...value] - : typeof value === "object" && value !== null - ? { ...value } - : value; - overrideHookState(fiber, hookId, [], stateValue); - } - } catch {} - } - - // Then handle unnamed state hooks - let hookIndex = 0; - let currentHook = fiber.memoizedState; - while (currentHook !== null) { - try { - const hookId = hookIndex.toString(); - const value = currentHook.memoizedState; - - // Only update if this hook isn't already handled by named states - if (!stateNames.includes(hookId)) { - // For arrays and objects, we need to clone to trigger updates - const stateValue = Array.isArray(value) - ? [...value] - : typeof value === "object" && value !== null - ? { ...value } - : value; - overrideHookState(fiber, hookId, [], stateValue); - } - } catch {} - - currentHook = currentHook.next as typeof currentHook; - hookIndex++; - } - } - - // Handle context updates - if (overrideContext) { - const contexts = getAllFiberContexts(fiber); - if (contexts) { - for (const [contextType, ctx] of contexts) { - try { - // Find the provider fiber for this context - let current: Fiber | null = fiber; - while (current) { - const type = current.type as { Provider?: unknown }; - if (type === contextType || type?.Provider === contextType) { - // Get the value we want to update to - const newValue = ctx.value; - if (newValue === undefined || newValue === null) break; - - // Only update if the value has actually changed - const currentValue = current.memoizedProps?.value; - if (isEqual(currentValue, newValue)) break; - - // Update the provider's value prop - overrideProps(current, ["value"], newValue); - if (current.alternate) { - overrideProps(current.alternate, ["value"], newValue); - } - break; - } - current = current.return; - } - } catch {} - } - } - } - - // Recursively handle children - let child = fiber.child; - while (child) { - await replayComponent(child); - child = child.sibling; - } - } catch {} -}; - export const extractMinimalFiberInfo = (fiber: Fiber): MinimalFiberInfo => { const timings = getTimings(fiber); return { diff --git a/packages/scan/src/web/views/inspector/what-changed.tsx b/packages/scan/src/web/views/inspector/what-changed.tsx index c04b4f14..711040c6 100644 --- a/packages/scan/src/web/views/inspector/what-changed.tsx +++ b/packages/scan/src/web/views/inspector/what-changed.tsx @@ -1,10 +1,20 @@ -import { type ReactNode, memo } from "preact/compat"; -import { useEffect, useRef, useState } from "preact/hooks"; -import { CopyToClipboard } from "~web/components/copy-to-clipboard"; -import { Icon } from "~web/components/icon"; -import { cn, throttle } from "~web/utils/helpers"; +import { + For, + Index, + Show, + createEffect, + createMemo, + createSignal, + on, + onCleanup, + type JSX, + type Setter, +} from "solid-js"; +import { CopyToClipboard } from "../../components/copy-to-clipboard"; +import { Icon } from "../../components/icon"; +import { cn, throttle } from "../../utils/helpers"; import { DiffValueView } from "./diff-value"; -import { timelineState } from "./states"; +import { getTimelineState } from "./states"; import { AggregatedChanges, formatFunctionPreview, @@ -14,87 +24,93 @@ import { } from "./utils"; import { calculateTotalChanges, - useInspectedFiberChangeStore, + createInspectedFiberChangeStore, } from "./whats-changed/use-change-store"; import { getDisplayName, getType } from "bippy"; -import { Store } from "~core/index"; +import { getInspectState } from "../../../core/native-state"; -export const WhatChanged = /* @__PURE__ */ memo(() => { - const [isExpanded, setIsExpanded] = useState(true); - const aggregatedChanges = useInspectedFiberChangeStore(); +export const WhatChanged = () => { + const [isExpanded, setIsExpanded] = createSignal(true); + const aggregatedChanges = createInspectedFiberChangeStore(); - const [hasInitialized, setHasInitialized] = useState(false); - const hasAnyChanges = calculateTotalChanges(aggregatedChanges) > 0; - useEffect(() => { - if (!hasInitialized && hasAnyChanges) { + const [hasInitialized, setHasInitialized] = createSignal(false); + const hasAnyChanges = createMemo(() => calculateTotalChanges(aggregatedChanges()) > 0); + createEffect(() => { + if (!hasInitialized() && hasAnyChanges()) { const timer = setTimeout(() => { setHasInitialized(true); requestAnimationFrame(() => { setIsExpanded(true); }); }, 0); - return () => clearTimeout(timer); + onCleanup(() => clearTimeout(timer)); } - }, [hasInitialized, hasAnyChanges]); - - const initializedContextChanges = new Map( - Array.from(aggregatedChanges.contextChanges.entries()) - .filter(([, value]) => value.kind === "initialized") - .map(([key, value]) => [ - key, - // oxlint-disable-next-line typescript/no-non-null-assertion - value.kind === "partially-initialized" ? null! : value.changes, - ]), + }); + + const initializedContextChanges = createMemo( + () => + new Map( + Array.from(aggregatedChanges().contextChanges.entries()).flatMap(([key, value]) => + value.kind === "initialized" ? [[key, value.changes]] : [], + ), + ), ); - const fiber = Store.inspectState.value.kind === "focused" ? Store.inspectState.value.fiber : null; + const fiber = createMemo(() => { + const inspectState = getInspectState(); + return inspectState.kind === "focused" ? inspectState.fiber : null; + }); - if (!fiber) { - // invariant - return; - } return ( - <> - - -
-
- - Why did {getDisplayName(fiber)} render? - - {!hasAnyChanges && ( -
-
No changes detected since selecting
-
- The props, state, and context changes within your component will be reported here -
+ + {(currentFiber) => ( + <> + + +
+
+ + Why did {getDisplayName(currentFiber())} render? + + +
+
No changes detected since selecting
+
+ The props, state, and context changes within your component will be reported + here +
+
+
- )} -
-
-
-
- renderStateName(name, getDisplayName(getType(fiber)) ?? "Unknown Component") - } - changes={aggregatedChanges.stateChanges} - title="Changed State" - isExpanded={isExpanded} - /> -
-
-
- +
+
+
+ renderStateName( + name, + getDisplayName(getType(currentFiber())) ?? "Unknown Component", + ) + } + changes={aggregatedChanges().stateChanges} + title="Changed State" + isExpanded={isExpanded()} + /> +
+
+
+ + )} + ); -}); +}; const renderStateName = (key: string, componentName: string) => { if (Number.isNaN(Number(key))) { @@ -121,531 +137,533 @@ const renderStateName = (key: string, componentName: string) => { }; return ( - - + + {n} {getOrdinalSuffix(n)} hook{" "} - called in {componentName} + called in {componentName} ); }; -const WhatsChangedHeader = memo(() => { - const refProps = useRef(null); - const refState = useRef(null); - const refContext = useRef(null); +const WhatsChangedHeader = () => { + let propsElement: HTMLDivElement | undefined; + let stateElement: HTMLDivElement | undefined; + let contextElement: HTMLDivElement | undefined; - const refStats = useRef<{ + let stats: { isPropsChanged: boolean; isStateChanged: boolean; isContextChanged: boolean; - }>({ + } = { isPropsChanged: false, isStateChanged: false, isContextChanged: false, - }); + }; + + const flash = throttle(() => { + const flashElements = []; + if (propsElement?.dataset.flash === "true") { + flashElements.push(propsElement); + } + if (stateElement?.dataset.flash === "true") { + flashElements.push(stateElement); + } + if (contextElement?.dataset.flash === "true") { + flashElements.push(contextElement); + } - useEffect(() => { - const flash = throttle(() => { - const flashElements = []; - if (refProps.current?.dataset.flash === "true") { - flashElements.push(refProps.current); - } - if (refState.current?.dataset.flash === "true") { - flashElements.push(refState.current); - } - if (refContext.current?.dataset.flash === "true") { - flashElements.push(refContext.current); - } - - for (const element of flashElements) { - element.classList.remove("count-flash-white"); - void element.offsetWidth; - element.classList.add("count-flash-white"); - } - }, 400); - - const unsubscribe = timelineState.subscribe((state) => { - if (!refProps.current || !refState.current || !refContext.current) { - return; - } - - const { currentIndex, updates } = state; - const currentUpdate = updates[currentIndex]; - - if (!currentUpdate || currentIndex === 0) { - return; - } - - flash(); - - refStats.current = { - isPropsChanged: (currentUpdate.props?.changes?.size ?? 0) > 0, - isStateChanged: (currentUpdate.state?.changes?.size ?? 0) > 0, - isContextChanged: (currentUpdate.context?.changes?.size ?? 0) > 0, - }; - - if (refProps.current.dataset.flash !== "true") { - refProps.current.dataset.flash = refStats.current.isPropsChanged.toString(); - } - if (refState.current.dataset.flash !== "true") { - refState.current.dataset.flash = refStats.current.isStateChanged.toString(); - } - if (refContext.current.dataset.flash !== "true") { - refContext.current.dataset.flash = refStats.current.isContextChanged.toString(); - } - }); - - return unsubscribe; - }, []); + for (const element of flashElements) { + element.classList.remove("count-flash-white"); + void element.offsetWidth; + element.classList.add("count-flash-white"); + } + }, 400); + + createEffect( + on( + getTimelineState, + (state) => { + if (!propsElement || !stateElement || !contextElement) { + return; + } + + const { currentIndex, updates } = state; + const currentUpdate = updates[currentIndex]; + + if (!currentUpdate || currentIndex === 0) { + return; + } + + flash(); + + stats = { + isPropsChanged: (currentUpdate.props?.changes?.size ?? 0) > 0, + isStateChanged: (currentUpdate.state?.changes?.size ?? 0) > 0, + isContextChanged: (currentUpdate.context?.changes?.size ?? 0) > 0, + }; + + if (propsElement.dataset.flash !== "true") { + propsElement.dataset.flash = stats.isPropsChanged.toString(); + } + if (stateElement.dataset.flash !== "true") { + stateElement.dataset.flash = stats.isStateChanged.toString(); + } + if (contextElement.dataset.flash !== "true") { + contextElement.dataset.flash = stats.isContextChanged.toString(); + } + }, + { defer: true }, + ), + ); return ( ); -}); +}; interface SectionProps { title: string; isExpanded: boolean; // oxlint-disable-next-line typescript/no-explicit-any changes: Map; - renderName?: (name: string) => ReactNode; + renderName?: (name: string) => JSX.Element; } const identity = (x: T) => x; -const Section = /* @__PURE__ */ memo(({ title, changes, renderName = identity }: SectionProps) => { - const [expandedFns, setExpandedFns] = useState(new Set()); - const [expandedEntries, setExpandedEntries] = useState(new Set()); +const Section = (props: SectionProps) => { + const [expandedFunctions, setExpandedFunctions] = createSignal(new Set()); + const [expandedEntries, setExpandedEntries] = createSignal(new Set()); + const entryKeys = createMemo(() => Array.from(props.changes.keys())); + const renderName = (name: string) => (props.renderName ?? identity)(name); - const entries = Array.from(changes.entries()); - - if (changes.size === 0) { - return null; - } return ( -
-
{title}
-
- {entries.map(([entryKey, change]) => { - const isEntryExpanded = expandedEntries.has(String(entryKey)); - const { value: prevValue, error: prevError } = safeGetValue(change.previousValue); - const { value: currValue, error: currError } = safeGetValue(change.currentValue); - - const diff = getObjectDiff(prevValue, currValue); - - return ( -
- -
-
-
- {prevError || currError ? ( - - ) : diff.changes.length > 0 ? ( - - ) : ( - - )} -
-
-
-
- ); - })} + 0}> +
+
{props.title}
+
+ + {(entryKey) => { + const isEntryExpanded = () => expandedEntries().has(String(entryKey)); + const change = createMemo(() => props.changes.get(entryKey)); + const previousResult = createMemo(() => safeGetValue(change()?.previousValue)); + const currentResult = createMemo(() => safeGetValue(change()?.currentValue)); + const diff = createMemo(() => + getObjectDiff(previousResult().value, currentResult().value), + ); + + return ( + + {(currentChange) => ( +
+ +
+
+
+ + } + > + 0} + fallback={ + + } + > + + + +
+
+
+
+ )} +
+ ); + }} +
+
-
+ ); -}); +}; -const AccessError = ({ prevError, currError }: { prevError?: string; currError?: string }) => { +interface AccessErrorProps { + prevError?: string; + currError?: string; +} + +const AccessError = (props: AccessErrorProps) => { return ( <> - {prevError && ( -
- {prevError} + +
+ {props.prevError}
- )} - {currError && ( -
- {currError} + + +
+ {props.currError}
- )} +
); }; -const DiffChange = ({ - diff, - title, - renderName, - change, - expandedFns, - setExpandedFns, -}: { +interface DiffChangeProps { diff: { - changes: { + changes: Array<{ path: string[]; prevValue: unknown; currentValue: unknown; - }[]; + }>; }; title: string; - renderName: (name: string) => ReactNode; + renderName: (name: string) => JSX.Element; change: { name: string }; - expandedFns: Set; - setExpandedFns: (updater: (prev: Set) => Set) => void; -}) => { - return diff.changes.map((diffChange, i) => { - const { value: prevDiffValue, error: prevDiffError } = safeGetValue(diffChange.prevValue); - const { value: currDiffValue, error: currDiffError } = safeGetValue(diffChange.currentValue); - - const isFunction = typeof prevDiffValue === "function" || typeof currDiffValue === "function"; - - let path: string | undefined; - - if (title === "Props") { - path = - diffChange.path.length > 0 - ? `${renderName(String(change.name))}.${formatPath(diffChange.path)}` - : undefined; - } - if (title === "State" && diffChange.path.length > 0) { - path = `state.${formatPath(diffChange.path)}`; - } - - if (!path) { - path = formatPath(diffChange.path); - } + expandedFunctions: Set; + setExpandedFunctions: Setter>; +} - return ( -
- {path &&
{path}
} - - -
- ); - }); + +
+
+ ); + }} + + ); }; -const ReferenceOnlyChange = ({ - prevValue, - currValue, - entryKey, - expandedFns, - setExpandedFns, -}: { +interface ReferenceOnlyChangeProps { prevValue: unknown; currValue: unknown; entryKey: string | number; - expandedFns: Set; - setExpandedFns: (updater: (prev: Set) => Set) => void; -}) => { + expandedFunctions: Set; + setExpandedFunctions: Setter>; +} + +const ReferenceOnlyChange = (props: ReferenceOnlyChangeProps) => { return ( <> -
- - - +
+ - + { - const key = `${String(entryKey)}-prev`; - setExpandedFns((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); + const key = `${String(props.entryKey)}-prev`; + props.setExpandedFunctions((previousFunctions) => { + const nextFunctions = new Set(previousFunctions); + if (nextFunctions.has(key)) { + nextFunctions.delete(key); } else { - next.add(key); + nextFunctions.add(key); } - return next; + return nextFunctions; }); }} isNegative={true} />
-
- + - +
+ + + { - const key = `${String(entryKey)}-current`; - setExpandedFns((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); + const key = `${String(props.entryKey)}-current`; + props.setExpandedFunctions((previousFunctions) => { + const nextFunctions = new Set(previousFunctions); + if (nextFunctions.has(key)) { + nextFunctions.delete(key); } else { - next.add(key); + nextFunctions.add(key); } - return next; + return nextFunctions; }); }} isNegative={false} />
- {typeof currValue === "object" && currValue !== null && ( -
- + +
+ Reference changed but objects are structurally the same
- )} +
); }; -const CountBadge = ({ - count, - forceFlash, - isFunction, - showWarning, -}: { +interface CountBadgeProps { count: number; forceFlash: boolean; isFunction: boolean; showWarning: boolean; -}) => { - const refIsFirstRender = useRef(true); - const refBadge = useRef(null); - const refPrevCount = useRef(count); - - useEffect(() => { - const element = refBadge.current; - if (!element || refPrevCount.current === count) { +} + +const CountBadge = (props: CountBadgeProps) => { + let isFirstRender = true; + let badgeElement: HTMLDivElement | undefined; + let previousCount = props.count; + + createEffect(() => { + const count = props.count; + const element = badgeElement; + if (!element || previousCount === count) { return; } @@ -653,35 +671,38 @@ const CountBadge = ({ void element.offsetWidth; element.classList.add("count-flash"); - refPrevCount.current = count; - }, [count]); + previousCount = count; + }); - useEffect(() => { - if (refIsFirstRender.current) { - refIsFirstRender.current = false; + createEffect(() => { + const shouldForceFlash = props.forceFlash; + if (isFirstRender) { + isFirstRender = false; return; } - if (forceFlash) { + if (shouldForceFlash) { let timer = setTimeout(() => { - refBadge.current?.classList.add("count-flash-white"); + badgeElement?.classList.add("count-flash-white"); timer = setTimeout(() => { - refBadge.current?.classList.remove("count-flash-white"); + badgeElement?.classList.remove("count-flash-white"); }, 300); }, 500); - return () => { + onCleanup(() => { clearTimeout(timer); - }; + }); } - }, [forceFlash]); + }); return ( -
- {showWarning && ( - - )} - {isFunction && }x - {count} +
+ + + + + + + x{props.count}
); }; diff --git a/packages/scan/src/web/views/inspector/whats-changed/use-change-store.ts b/packages/scan/src/web/views/inspector/whats-changed/use-change-store.ts index 7cc3c023..26221ed9 100644 --- a/packages/scan/src/web/views/inspector/whats-changed/use-change-store.ts +++ b/packages/scan/src/web/views/inspector/whats-changed/use-change-store.ts @@ -1,20 +1,38 @@ -import { useEffect, useRef, useState } from "preact/hooks"; -import { ChangesListener, ChangesPayload, ContextChange, Store } from "~core/index"; +import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"; +import { + type ChangesListener, + type ChangesPayload, + type ContextChange, +} from "../../../../core/index"; +import { Store, getInspectState } from "../../../../core/native-state"; import { getFiberId } from "bippy"; -import { isEqual } from "~core/utils"; +import { isEqual } from "../../../../core/utils"; const CHANGES_QUEUE_INTERVAL = 50; -export type AggregatedChanges = { +export interface AggregatedChanges { count: number; currentValue: unknown; previousValue: unknown; name: string; lastUpdated: number; id: string; -}; +} + +interface InitializedContextChanges { + changes: AggregatedChanges; + kind: "initialized"; +} + +interface PartiallyInitializedContextChanges { + kind: "partially-initialized"; + value: unknown; + name: string; + lastUpdated: number; + id: string; +} -export type AllAggregatedChanges = { +export interface AllAggregatedChanges { // oxlint-disable-next-line typescript/no-explicit-any propsChanges: Map; // oxlint-disable-next-line typescript/no-explicit-any @@ -22,19 +40,9 @@ export type AllAggregatedChanges = { contextChanges: Map< // oxlint-disable-next-line typescript/no-explicit-any any, - | { changes: AggregatedChanges; kind: "initialized" } - | { - // this looks weird, because it is - // its a work around to allow context changes to be sent impotently - // (react-scan internals do not yet handle sending context changes the render they change) - kind: "partially-initialized"; - value: unknown; - name: string; - lastUpdated: number; - id: string; - } + InitializedContextChanges | PartiallyInitializedContextChanges >; -}; +} const getContextChangesValue = ( discriminated: @@ -307,28 +315,27 @@ export const calculateTotalChanges = (changes: AllAggregatedChanges) => { ); }; -export const useInspectedFiberChangeStore = (opts?: { +export const createInspectedFiberChangeStore = (opts?: { onChangeUpdate?: (countUpdated: number) => void; }) => { - const pendingChanges = useRef<{ queue: ChangesPayload[] }>({ queue: [] }); - // flushed state read from queue stream - const [aggregatedChanges, setAggregatedChanges] = useState({ + const pendingChanges: { queue: ChangesPayload[] } = { queue: [] }; + const [aggregatedChanges, setAggregatedChanges] = createSignal({ propsChanges: new Map(), stateChanges: new Map(), contextChanges: new Map(), }); - const fiber = Store.inspectState.value.kind === "focused" ? Store.inspectState.value.fiber : null; - const fiberId = fiber ? getFiberId(fiber) : null; + const fiberId = createMemo(() => { + const inspectState = getInspectState(); + return inspectState.kind === "focused" ? getFiberId(inspectState.fiber) : null; + }); - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { + onMount(() => { const interval = setInterval(() => { - // optimization to avoid unconditional renders - if (pendingChanges.current.queue.length === 0) return; + if (pendingChanges.queue.length === 0) return; setAggregatedChanges((prevAggregatedChanges) => { - const queueChanges = collapseQueue(pendingChanges.current.queue); + const queueChanges = collapseQueue(pendingChanges.queue); const merged = mergeChanges(prevAggregatedChanges, queueChanges); const prevTotal = calculateTotalChanges(prevAggregatedChanges); const newTotal = calculateTotalChanges(merged); @@ -338,58 +345,47 @@ export const useInspectedFiberChangeStore = (opts?: { return merged; }); - pendingChanges.current.queue = []; + pendingChanges.queue = []; }, CHANGES_QUEUE_INTERVAL); - return () => { + onCleanup(() => { clearInterval(interval); - }; - }, [fiber]); + }); + }); - // un-throttled subscription - useEffect(() => { - if (!fiberId) { + createEffect(() => { + const currentFiberId = fiberId(); + if (!currentFiberId) { return; } const listener: ChangesListener = (change) => { - pendingChanges.current?.queue.push(change); + pendingChanges.queue.push(change); }; - let listeners = Store.changesListeners.get(fiberId); + let listeners = Store.changesListeners.get(currentFiberId); if (!listeners) { listeners = []; - Store.changesListeners.set(fiberId, listeners); + Store.changesListeners.set(currentFiberId, listeners); } listeners.push(listener); - return () => { + onCleanup(() => { setAggregatedChanges({ propsChanges: new Map(), stateChanges: new Map(), contextChanges: new Map(), }); - pendingChanges.current.queue = []; + pendingChanges.queue = []; Store.changesListeners.set( - fiberId, - Store.changesListeners.get(fiberId)?.filter((l) => l !== listener) ?? [], + currentFiberId, + Store.changesListeners + .get(currentFiberId) + ?.filter((candidateListener) => candidateListener !== listener) ?? [], ); - }; - }, [fiberId]); - - // cleanup - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - return () => { - setAggregatedChanges({ - propsChanges: new Map(), - stateChanges: new Map(), - contextChanges: new Map(), - }); - pendingChanges.current.queue = []; - }; - }, [fiberId]); + }); + }); return aggregatedChanges; }; diff --git a/packages/scan/src/web/views/notifications/collapsed-event.tsx b/packages/scan/src/web/views/notifications/collapsed-event.tsx index 34365b69..5b8c5425 100644 --- a/packages/scan/src/web/views/notifications/collapsed-event.tsx +++ b/packages/scan/src/web/views/notifications/collapsed-event.tsx @@ -1,184 +1,150 @@ -import type { JSX } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; import { - DroppedFramesEvent, - getComponentName, - getEventSeverity, - InteractionEvent, -} from './data'; -import { SlowdownHistoryItem } from './slowdown-history'; -import { ChevronRight } from './icons'; -import { cn } from '~web/utils/helpers'; + For, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, + type Accessor, + type JSX, +} from "solid-js"; +import { cn } from "../../utils/helpers"; +import { DroppedFramesEvent, InteractionEvent, getComponentName, getEventSeverity } from "./data"; +import { ChevronRight } from "./icons"; +import { SlowdownHistoryItem } from "./slowdown-history"; -export type CollapsedDroppedFrame = { - kind: 'collapsed-frame-drops'; +export interface CollapsedDroppedFrame { + kind: "collapsed-frame-drops"; events: Array; timestamp: number; -}; +} -type CollapsedKeyboardInput = { - kind: 'collapsed-keyboard'; +export interface CollapsedKeyboardInput { + kind: "collapsed-keyboard"; events: Array; timestamp: number; -}; -const useNestedFlash = ({ - flashingItemsCount, - totalEvents, -}: { - totalEvents: number; // this breaks if you have constant 1 item flashing, but the actual item is different over time (it's fine for now) - flashingItemsCount: number; -}) => { - const [newFlash, setNewFlash] = useState(false); - const flashedFor = useRef(0); - const lastFlashTime = useRef(0); +} - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - if (flashedFor.current >= totalEvents) { +const useNestedFlash = (flashingItemsCount: Accessor, totalEvents: Accessor) => { + const [isFlashing, setIsFlashing] = createSignal(false); + let flashedFor = 0; + let lastFlashTime = 0; + + createEffect(() => { + flashingItemsCount(); + const eventCount = totalEvents(); + if (flashedFor >= eventCount) { return; } - const now = Date.now(); - const debounceTime = 250; - const timeSinceLastFlash = now - lastFlashTime.current; - - if (timeSinceLastFlash >= debounceTime) { - setNewFlash(false); - const timeout = setTimeout(() => { - flashedFor.current = totalEvents; - lastFlashTime.current = Date.now(); - setNewFlash(true); - // horrible, don't look at this, move along - setTimeout(() => { - setNewFlash(false); - }, 2000); + const delay = Math.max(0, 250 - (Date.now() - lastFlashTime)); + let flashTimer: ReturnType | undefined; + let stopTimer: ReturnType | undefined; + const startTimer = setTimeout(() => { + setIsFlashing(false); + flashTimer = setTimeout(() => { + flashedFor = eventCount; + lastFlashTime = Date.now(); + setIsFlashing(true); + stopTimer = setTimeout(() => setIsFlashing(false), 2000); }, 50); - return () => clearTimeout(timeout); - } else { - const delayNeeded = debounceTime - timeSinceLastFlash; - const timeout = setTimeout(() => { - setNewFlash(false); - setTimeout(() => { - flashedFor.current = totalEvents; - lastFlashTime.current = Date.now(); - setNewFlash(true); - // horrible, don't look at this, move along - setTimeout(() => { - setNewFlash(false); - }, 2000); - }, 50); - }, delayNeeded); - return () => clearTimeout(timeout); - } - }, [flashingItemsCount]); + }, delay); + + onCleanup(() => { + clearTimeout(startTimer); + if (flashTimer) clearTimeout(flashTimer); + if (stopTimer) clearTimeout(stopTimer); + }); + }); - return newFlash; + return isFlashing; }; -export const CollapsedItem = ({ - item, - shouldFlash, -}: { +export const CollapsedItem = (props: { item: CollapsedDroppedFrame | CollapsedKeyboardInput; shouldFlash: (id: string) => boolean; }) => { - const [expanded, setExpanded] = useState(false); - - const severity = item.events.map(getEventSeverity).reduce((prev, curr) => { - switch (curr) { - case 'high': { - return 'high'; - } - case 'needs-improvement': { - return prev === 'high' ? 'high' : 'needs-improvement'; - } - case 'low': { - return prev; - } - } - }, 'low'); - const flashingItemsCount = item.events.reduce( - (prev, curr) => (shouldFlash(curr.id) ? prev + 1 : prev), - 0, + const [isExpanded, setIsExpanded] = createSignal(false); + const severity = createMemo(() => + props.item.events + .map(getEventSeverity) + .reduce<"low" | "needs-improvement" | "high">((previousSeverity, currentSeverity) => { + switch (currentSeverity) { + case "high": + return "high"; + case "needs-improvement": + return previousSeverity === "high" ? "high" : "needs-improvement"; + case "low": + return previousSeverity; + } + }, "low"), ); - - const shouldFlashAgain = useNestedFlash({ - flashingItemsCount, - totalEvents: item.events.length, - }); + const flashingItemsCount = createMemo(() => + props.item.events.reduce( + (flashingCount, event) => (props.shouldFlash(event.id) ? flashingCount + 1 : flashingCount), + 0, + ), + ); + const shouldFlashAgain = useNestedFlash(flashingItemsCount, () => props.item.events.length); return ( -
+
- {expanded && ( + - {item.events - .toSorted((a, b) => b.timestamp - a.timestamp) - .map((event) => ( - - ))} + second.timestamp - first.timestamp)} + > + {(event) => ( + + )} + - )} +
); }; -const IndentedContent = ({ - children, -}: { children: JSX.Element | JSX.Element[] }) => ( -
-
- {children} +const IndentedContent = (props: { children: JSX.Element }) => ( +
+
+ {props.children}
); diff --git a/packages/scan/src/web/views/notifications/data.ts b/packages/scan/src/web/views/notifications/data.ts index c94164b2..4e5be810 100644 --- a/packages/scan/src/web/views/notifications/data.ts +++ b/packages/scan/src/web/views/notifications/data.ts @@ -1,7 +1,6 @@ -import { createContext } from "preact"; -import { SetStateAction } from "preact/compat"; -import { Dispatch, useContext } from "preact/hooks"; -import { HIGH_SEVERITY_FPS_DROP_TIME } from "~core/notifications/event-tracking"; +import { createContext, useContext } from "solid-js"; +import type { SetStoreFunction } from "solid-js/store"; +import { HIGH_SEVERITY_FPS_DROP_TIME } from "../../../core/notifications/event-tracking"; export type GroupedFiberRender = { id: string; @@ -112,9 +111,8 @@ export type NotificationsState = { | "render-visualization" | "other-visualization" // | "render-guide" - | "render-explanation" - // | "other-guide" - | "optimize"; + | "render-explanation"; + // | "other-guide" /** * Conceptually a synthetic query parameter */ @@ -149,11 +147,9 @@ export const getEventSeverity = (event: NotificationEvent) => { } }; -export const useNotificationsContext = () => useContext(NotificationStateContext); - -export const NotificationStateContext = createContext<{ +export interface NotificationsContextValue { notificationState: NotificationsState; - setNotificationState: Dispatch>; + setNotificationState: SetStoreFunction; setRoute: ({ route, routeMessage, @@ -161,5 +157,14 @@ export const NotificationStateContext = createContext<{ route: NotificationsState["route"]; routeMessage: NotificationsState["routeMessage"] | null; }) => void; - // oxlint-disable-next-line typescript/no-non-null-assertion -}>(null!); +} + +export const NotificationStateContext = createContext(); + +export const useNotificationsContext = () => { + const context = useContext(NotificationStateContext); + if (!context) { + throw new Error("Notifications context is unavailable"); + } + return context; +}; diff --git a/packages/scan/src/web/views/notifications/details-routes.tsx b/packages/scan/src/web/views/notifications/details-routes.tsx index 7bd7b4a2..632f5059 100644 --- a/packages/scan/src/web/views/notifications/details-routes.tsx +++ b/packages/scan/src/web/views/notifications/details-routes.tsx @@ -1,207 +1,160 @@ -import { ReactNode, useEffect, useRef, useState } from 'preact/compat'; -import { playNotificationSound } from '~core/utils'; -import { cn } from '~web/utils/helpers'; -import { useNotificationsContext } from './data'; -import { CloseIcon } from './icons'; -import { NotificationTabs } from './notification-tabs'; -import { Optimize } from './optimize'; -import { OtherVisualization } from './other-visualization'; -import { RenderBarChart } from './render-bar-chart'; -import { RenderExplanation } from './render-explanation'; -import { signalWidgetViews } from '~web/state'; +import { Show, Switch, Match, createSignal, onCleanup, onMount, type JSX } from "solid-js"; +import { playNotificationSound } from "../../../core/utils"; +import { setWidgetView } from "../../state"; +import { cn } from "../../utils/helpers"; +import { useNotificationsContext } from "./data"; +import { CloseIcon } from "./icons"; +import { NotificationTabs } from "./notification-tabs"; +import { OtherVisualization } from "./other-visualization"; +import { RenderBarChart } from "./render-bar-chart"; +import { RenderExplanation } from "./render-explanation"; export const DetailsRoutes = () => { const { notificationState, setNotificationState } = useNotificationsContext(); - const [dots, setDots] = useState('...'); - const containerRef = useRef(null); + const [dots, setDots] = createSignal("..."); - useEffect(() => { + onMount(() => { const interval = setInterval(() => { - setDots((prev) => { - if (prev === '...') return ''; - return prev + '.'; - }); + setDots((previousDots) => (previousDots === "..." ? "" : `${previousDots}.`)); }, 500); - return () => clearInterval(interval); - }, []); + onCleanup(() => clearInterval(interval)); + }); - if (!notificationState.selectedEvent) { - return ( -
+ return ( + - -
-
-
-
- - Scanning for slowdowns - {dots} - -
- {notificationState.events.length !== 0 && ( -

- Click on an item in the{' '} - History list to - get started -

- )} -

- You don't need to keep this panel open for React Scan to record - slowdowns -

-

- Enable audio alerts to hear a delightful ding every time a large - slowdown is recorded -

-
-
- ); - } - - switch (notificationState.route) { - case 'render-visualization': { - return ( - - - - ); - } - case 'render-explanation': { - if (!notificationState.selectedFiber) { - // todo: dev only - throw new Error( - 'Invariant: must have selected fiber when viewing render explanation', - ); - } - return ( - - - - ); - } - - case 'other-visualization': { - return ( -
- +
+
+ + Scanning for slowdowns{dots()} + +
+ +

+ Click on an item in the History list + to get started +

+
+

+ You don't need to keep this panel open for React Scan to record slowdowns +

+

+ Enable audio alerts to hear a delightful ding every time a large slowdown is + recorded +

+ +
-
- ); - } - case 'optimize': { - return ( - - - - ); - } - } - // exhaustive verification - notificationState.route satisfies never; +
+ } + > + {(selectedEvent) => ( + + + + + + + + Unable to show render details
} + > + {(selectedFiber) => ( + + + + )} + + + + +
+ +
+
+
+ + )} + + ); }; -const TabLayout = ({ children }: { children: ReactNode }) => { +const TabLayout = (props: { children: JSX.Element }) => { const { notificationState } = useNotificationsContext(); - if (!notificationState.selectedEvent) { - // todo: dev only - throw new Error( - 'Invariant: d must have selected event when viewing render explanation', - ); - } + return ( -
-
- +
+
+ + {(selectedEvent) => } +
-
- {children} +
+ {props.children}
); diff --git a/packages/scan/src/web/views/notifications/icons.tsx b/packages/scan/src/web/views/notifications/icons.tsx index 6005629b..c5cf2bff 100644 --- a/packages/scan/src/web/views/notifications/icons.tsx +++ b/packages/scan/src/web/views/notifications/icons.tsx @@ -1,115 +1,112 @@ -import { ReactScanInternals } from "~core/index"; -import { cn } from "~web/utils/helpers"; +import { getOptionsState } from "../../../core/native-state"; +import { cn } from "../../utils/helpers"; -export const ChevronRight = ({ size = 24, className }: { size?: number; className?: string }) => ( +interface IconProps { + size?: number; + class?: string; +} + +interface NotificationProps extends IconProps { + events: Array; +} + +export const ChevronRight = (props: IconProps) => ( ); -export const Notification = ({ - className = "", - size = 24, - events = [], -}: { - className?: string; - size?: number; - events: boolean[]; -}) => { - const hasHighSeverity = events.includes(true); - const totalSevere = events.filter((e) => e).length; - const displayCount = totalSevere > 99 ? ">99" : totalSevere; - const badgeSize = hasHighSeverity ? Math.max(size * 0.6, 14) : Math.max(size * 0.4, 6); +export const Notification = (props: NotificationProps) => { + const hasHighSeverity = () => props.events.includes(true); + const totalSevere = () => props.events.filter((isSevere) => isSevere).length; + const displayCount = () => (totalSevere() > 99 ? ">99" : totalSevere()); + const badgeSize = () => + hasHighSeverity() + ? Math.max((props.size ?? 24) * 0.6, 14) + : Math.max((props.size ?? 24) * 0.4, 6); return ( -
+
- {events.length > 0 && - totalSevere > 0 && - ReactScanInternals.options.value.showNotificationCount && ( -
- {hasHighSeverity && displayCount} -
- )} + {props.events.length > 0 && totalSevere() > 0 && getOptionsState().showNotificationCount && ( +
+ {hasHighSeverity() && displayCount()} +
+ )}
); }; -export const CloseIcon = ({ className = "", size = 24 }: { className?: string; size?: number }) => ( +export const CloseIcon = (props: IconProps) => ( ); -export const VolumeOnIcon = ({ - className = "", - size = 24, -}: { - className?: string; - size?: number; -}) => ( +export const VolumeOnIcon = (props: IconProps) => ( @@ -117,24 +114,18 @@ export const VolumeOnIcon = ({ ); -export const VolumeOffIcon = ({ - className = "", - size = 24, -}: { - className?: string; - size?: number; -}) => ( +export const VolumeOffIcon = (props: IconProps) => ( @@ -144,42 +135,36 @@ export const VolumeOffIcon = ({ ); -export const ArrowLeft = ({ size = 24, className }: { size?: number; className?: string }) => ( +export const ArrowLeft = (props: IconProps) => ( ); -export const PointerIcon = ({ - className = "", - size = 24, -}: { - className?: string; - size?: number; -}) => ( +export const PointerIcon = (props: IconProps) => ( @@ -189,24 +174,18 @@ export const PointerIcon = ({ ); -export const KeyboardIcon = ({ - className = "", - size = 24, -}: { - className?: string; - size?: number; -}) => ( +export const KeyboardIcon = (props: IconProps) => ( @@ -219,19 +198,19 @@ export const KeyboardIcon = ({ ); -export const ClearIcon = ({ className = "", size = 24 }: { className?: string; size?: number }) => { +export const ClearIcon = (props: IconProps) => { return ( @@ -239,24 +218,18 @@ export const ClearIcon = ({ className = "", size = 24 }: { className?: string; s ); }; -export const TrendingDownIcon = ({ - className = "", - size = 24, -}: { - className?: string; - size?: number; -}) => ( +export const TrendingDownIcon = (props: IconProps) => ( diff --git a/packages/scan/src/web/views/notifications/notification-header.tsx b/packages/scan/src/web/views/notifications/notification-header.tsx index b90c0fcd..9e3b0a95 100644 --- a/packages/scan/src/web/views/notifications/notification-header.tsx +++ b/packages/scan/src/web/views/notifications/notification-header.tsx @@ -1,121 +1,98 @@ -import { cn } from '~web/utils/helpers'; -import { - NotificationEvent, - getComponentName, - getEventSeverity, - getTotalTime, -} from './data'; -import { CloseIcon } from './icons'; -import { signalWidgetViews } from '~web/state'; +import { cn } from "../../utils/helpers"; +import { NotificationEvent, getComponentName, getEventSeverity, getTotalTime } from "./data"; +import { CloseIcon } from "./icons"; +import { setWidgetView } from "../../state"; +import { createMemo } from "solid-js"; -export const NotificationHeader = ({ - selectedEvent, -}: { - selectedEvent: NotificationEvent; -}) => { - const severity = getEventSeverity(selectedEvent); - switch (selectedEvent.kind) { - case 'interaction': { - return ( - // h-[48px] is a hack to adjust for header size -
- {/* todo: make css variables for colors */} -
-
- - {selectedEvent.type === 'click' ? 'Clicked ' : 'Typed in '} - - {getComponentName(selectedEvent.componentPath)} -
- {getTotalTime(selectedEvent.timing).toFixed(0)}ms processing - time -
-
+export const NotificationHeader = (props: { selectedEvent: NotificationEvent }) => { + const content = createMemo(() => { + const selectedEvent = props.selectedEvent; + const severity = getEventSeverity(selectedEvent); + switch (selectedEvent.kind) { + case "interaction": { + return ( + // h-[48px] is a hack to adjust for header size +
+ {/* todo: make css variables for colors */}
-
- + {getTotalTime(selectedEvent.timing).toFixed(0)}ms processing time +
-
-
-
- ); - } - case 'dropped-frames': { - return ( -
-
-
- FPS Drop -
- dropped to {selectedEvent.fps} FPS +
+
+ +
- +
+ ); + } + case "dropped-frames": { + return ( +
-
- + dropped to {selectedEvent.fps} FPS +
+
+ +
+
+ +
-
- ); + ); + } } - } + }); + return <>{content()}; }; diff --git a/packages/scan/src/web/views/notifications/notification-tabs.tsx b/packages/scan/src/web/views/notifications/notification-tabs.tsx index 73179555..938d4b38 100644 --- a/packages/scan/src/web/views/notifications/notification-tabs.tsx +++ b/packages/scan/src/web/views/notifications/notification-tabs.tsx @@ -1,40 +1,27 @@ -import { cn } from '~web/utils/helpers'; -import { NotificationEvent, useNotificationsContext } from './data'; -import { Popover } from './popover'; -import { VolumeOffIcon, VolumeOnIcon } from './icons'; -import { playNotificationSound } from '~core/utils'; +import { cn } from "../../utils/helpers"; +import { NotificationEvent, useNotificationsContext } from "./data"; +import { Popover } from "./popover"; +import { VolumeOffIcon, VolumeOnIcon } from "./icons"; +import { playNotificationSound } from "../../../core/utils"; -export const NotificationTabs = ({ - selectedEvent: _, -}: { - selectedEvent: NotificationEvent; -}) => { - const { notificationState, setNotificationState, setRoute } = - useNotificationsContext(); +export const NotificationTabs = (_props: { selectedEvent: NotificationEvent }) => { + const { notificationState, setNotificationState, setRoute } = useNotificationsContext(); return ( -
-
+
+
-
{ - setNotificationState((prev) => { - if ( - prev.audioNotificationsOptions.enabled && - prev.audioNotificationsOptions.audioContext.state !== 'closed' - ) { - prev.audioNotificationsOptions.audioContext.close(); - } - const prevEnabledState = prev.audioNotificationsOptions.enabled; - localStorage.setItem( - 'react-scan-notifications-audio', - String(!prevEnabledState), - ); + const audioOptions = notificationState.audioNotificationsOptions; + if (audioOptions.enabled && audioOptions.audioContext.state !== "closed") { + void audioOptions.audioContext.close(); + } + localStorage.setItem("react-scan-notifications-audio", String(!audioOptions.enabled)); + + if (audioOptions.enabled) { + setNotificationState("audioNotificationsOptions", { + audioContext: null, + enabled: false, + }); + return; + } - const audioContext = new AudioContext(); - if (!prev.audioNotificationsOptions.enabled) { - playNotificationSound(audioContext); - } - if (prevEnabledState) { - audioContext.close(); - } - return { - ...prev, - audioNotificationsOptions: prevEnabledState - ? { - audioContext: null, - enabled: false, - } - : { - audioContext, - enabled: true, - }, - }; + const audioContext = new AudioContext(); + playNotificationSound(audioContext); + setNotificationState("audioNotificationsOptions", { + audioContext, + enabled: true, }); }} - className="ml-auto" + class="ml-auto" > -
+
Alerts {notificationState.audioNotificationsOptions.enabled ? ( - + ) : ( - + )}
diff --git a/packages/scan/src/web/views/notifications/notifications.tsx b/packages/scan/src/web/views/notifications/notifications.tsx index 1a0d1725..d30af7bc 100644 --- a/packages/scan/src/web/views/notifications/notifications.tsx +++ b/packages/scan/src/web/views/notifications/notifications.tsx @@ -1,10 +1,23 @@ -import { forwardRef } from "preact/compat"; -import { useEffect, useRef, useState } from "preact/hooks"; -import { not_globally_unique_generateId, playNotificationSound } from "~core/utils"; -import { useToolbarEventLog } from "~core/notifications/event-tracking"; -import { FiberRenders } from "~core/notifications/performance"; -import { iife, invariantError } from "~core/notifications/performance-utils"; -import { cn } from "~web/utils/helpers"; +import { + For, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, + type Accessor, + type JSX, +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { + getToolbarEvents, + subscribeToolbarEvents, +} from "../../../core/notifications/event-tracking"; +import { FiberRenders } from "../../../core/notifications/performance"; +import { invariantError } from "../../../core/notifications/performance-utils"; +import { not_globally_unique_generateId, playNotificationSound } from "../../../core/utils"; +import { cn } from "../../utils/helpers"; import { NotificationStateContext, NotificationsState, @@ -17,18 +30,23 @@ import { NotificationHeader } from "./notification-header"; import { fadeOutHighlights } from "./render-bar-chart"; import { SlowdownHistory, useLaggedEvents } from "./slowdown-history"; -const getGroupedFiberRenders = (fiberRenders: FiberRenders) => { - const res = Object.values(fiberRenders).map((render) => ({ +const AUDIO_DEBOUNCE_MS = 1000; +const ELEMENT_GARBAGE_COLLECTION_INTERVAL_MS = 5000; + +const getGroupedFiberRenders = (fiberRenders: FiberRenders) => + Object.values(fiberRenders).map((render) => ({ id: not_globally_unique_generateId(), - totalTime: render.nodeInfo.reduce((prev, curr) => prev + curr.selfTime, 0), + totalTime: render.nodeInfo.reduce( + (totalTime, nodeInformation) => totalTime + nodeInformation.selfTime, + 0, + ), count: render.nodeInfo.length, - name: render.nodeInfo[0].name, // invariant, at least one exists, + name: render.nodeInfo[0].name, deletedAll: false, parents: render.parents, hasMemoCache: render.hasMemoCache, wasFiberRenderMount: render.wasFiberRenderMount, - // it would be nice if we calculated the % of components memoizable, but this would have to be calculated downstream before it got aggregated - elements: render.nodeInfo.map((node) => node.element), + elements: render.nodeInfo.map((nodeInformation) => nodeInformation.element), changes: { context: render.changes.fiberContext.current .filter((change) => render.changes.fiberContext.changesCounts.get(change.name)) @@ -45,218 +63,188 @@ const getGroupedFiberRenders = (fiberRenders: FiberRenders) => { state: render.changes.fiberState.current .filter((change) => render.changes.fiberState.changesCounts.get(Number(change.name))) .map((change) => ({ - index: change.name as number, + index: Number(change.name), count: render.changes.fiberState.changesCounts.get(Number(change.name)) ?? 0, })), }, })); - return res; -}; - -const useGarbageCollectElements = (notificationEvents: NotificationsState["events"]) => { - useEffect(() => { +const useGarbageCollectElements = (notificationEvents: Accessor) => { + onMount(() => { const checkElementsExistence = () => { - notificationEvents.forEach((event) => { - if (!event.groupedFiberRenders) return; - + notificationEvents().forEach((event) => { event.groupedFiberRenders.forEach((render) => { if (render.deletedAll) return; - - if (!render.elements || render.elements.length === 0) { + if (render.elements.length === 0) { render.deletedAll = true; return; } const initialLength = render.elements.length; - render.elements = render.elements.filter((element) => { - return element && element.isConnected; - }); - + render.elements = render.elements.filter((element) => element.isConnected); if (render.elements.length === 0 && initialLength > 0) { render.deletedAll = true; } }); }); }; - - const intervalId = setInterval(checkElementsExistence, 5000); - - return () => { - clearInterval(intervalId); - }; - }, [notificationEvents]); + const intervalId = setInterval(checkElementsExistence, ELEMENT_GARBAGE_COLLECTION_INTERVAL_MS); + onCleanup(() => clearInterval(intervalId)); + }); }; export const useAppNotifications = () => { - const log = useToolbarEventLog(); - - const notificationEvents: NotificationsState["events"] = []; - - useGarbageCollectElements(notificationEvents); - - log.state.events.forEach((event) => { - const fiberRenders = - event.kind === "interaction" - ? event.data.meta.detailedTiming.fiberRenders - : event.data.meta.fiberRenders; - const groupedFiberRenders = getGroupedFiberRenders(fiberRenders); - const renderTime = groupedFiberRenders.reduce((prev, curr) => prev + curr.totalTime, 0); - switch (event.kind) { - case "interaction": { - const { commitEnd, jsEndDetail, interactionStartDetail, rafStart } = - event.data.meta.detailedTiming; + const [eventLog, setEventLog] = createSignal(getToolbarEvents()); - // this is a known bug, js time doesn't backfill render time from async renders (or async js in general) - // the current impl is a close enough approximation so will leave as is until there is a dedicated effort to fix it - if (jsEndDetail - interactionStartDetail - renderTime < 0) { - invariantError("js time must be longer than render time"); - } - const otherJSTime = Math.max(0, jsEndDetail - interactionStartDetail - renderTime); + onMount(() => { + const unsubscribe = subscribeToolbarEvents(() => { + setEventLog(() => getToolbarEvents()); + }); + onCleanup(unsubscribe); + }); - const frameDraw = Math.max( - event.data.meta.latency - (commitEnd - interactionStartDetail), - 0, - ); - notificationEvents.push({ - componentPath: event.data.meta.detailedTiming.componentPath, - groupedFiberRenders, - id: event.id, - kind: "interaction", - memory: null, - timestamp: event.data.startAt, - type: - event.data.meta.detailedTiming.interactionType === "keyboard" ? "keyboard" : "click", - timing: { - renderTime: renderTime, + const notificationEvents = createMemo(() => { + const events: NotificationsState["events"] = []; + eventLog().forEach((event) => { + const fiberRenders = + event.kind === "interaction" + ? event.data.meta.detailedTiming.fiberRenders + : event.data.meta.fiberRenders; + const groupedFiberRenders = getGroupedFiberRenders(fiberRenders); + const renderTime = groupedFiberRenders.reduce( + (totalTime, render) => totalTime + render.totalTime, + 0, + ); + + switch (event.kind) { + case "interaction": { + const { commitEnd, jsEndDetail, interactionStartDetail, rafStart } = + event.data.meta.detailedTiming; + if (jsEndDetail - interactionStartDetail - renderTime < 0) { + invariantError("js time must be longer than render time"); + } + const otherJSTime = Math.max(0, jsEndDetail - interactionStartDetail - renderTime); + const frameDraw = Math.max( + event.data.meta.latency - (commitEnd - interactionStartDetail), + 0, + ); + events.push({ + componentPath: event.data.meta.detailedTiming.componentPath, + groupedFiberRenders, + id: event.id, kind: "interaction", - otherJSTime, - framePreparation: rafStart - jsEndDetail, - frameConstruction: commitEnd - rafStart, - frameDraw, - }, - }); - return; - } - case "long-render": { - notificationEvents.push({ - kind: "dropped-frames", - id: event.id, - memory: null, - timing: { + memory: null, + timestamp: event.data.startAt, + type: + event.data.meta.detailedTiming.interactionType === "keyboard" ? "keyboard" : "click", + timing: { + renderTime, + kind: "interaction", + otherJSTime, + framePreparation: rafStart - jsEndDetail, + frameConstruction: commitEnd - rafStart, + frameDraw, + }, + }); + break; + } + case "long-render": + events.push({ kind: "dropped-frames", - renderTime: renderTime, - otherTime: event.data.meta.latency, - }, - groupedFiberRenders, - timestamp: event.data.startAt, - fps: event.data.meta.fps, - }); - return; + id: event.id, + memory: null, + timing: { + kind: "dropped-frames", + renderTime, + otherTime: event.data.meta.latency, + }, + groupedFiberRenders, + timestamp: event.data.startAt, + fps: event.data.meta.fps, + }); + break; } - } + }); + return events; }); + + useGarbageCollectElements(notificationEvents); return notificationEvents; }; -const timeout = 1000; + const NotificationAudio = () => { const { notificationState, setNotificationState } = useNotificationsContext(); - const playedFor = useRef(null); - const debounceTimeout = useRef(null); - const lastPlayedTime = useRef(0); - const [laggedEvents] = useLaggedEvents(); + let playedFor: number | null = null; + let debounceTimeout: ReturnType | undefined; + let lastPlayedTime = 0; + const alertEventsCount = createMemo( + () => laggedEvents().filter((event) => getEventSeverity(event) === "high").length, + ); - const alertEventsCount = laggedEvents.filter( - // todo: make this configurable - (event) => getEventSeverity(event) === "high", - ).length; - - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - // todo: sync with options + onMount(() => { const audioEnabledString = localStorage.getItem("react-scan-notifications-audio"); - if (audioEnabledString !== "false" && audioEnabledString !== "true") { localStorage.setItem("react-scan-notifications-audio", "false"); return; } - - const audioEnabled = audioEnabledString === "false" ? false : true; - - if (audioEnabled) { - setNotificationState((prev) => { - if (prev.audioNotificationsOptions.enabled) { - return prev; - } - return { - ...prev, - audioNotificationsOptions: { - enabled: true, - audioContext: new AudioContext(), - }, - }; + if (audioEnabledString === "true" && !notificationState.audioNotificationsOptions.enabled) { + setNotificationState("audioNotificationsOptions", { + enabled: true, + audioContext: new AudioContext(), }); - return; } - }, []); + }); - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - const { audioNotificationsOptions } = notificationState; - if (!audioNotificationsOptions.enabled) { + createEffect(() => { + const eventCount = alertEventsCount(); + const audioOptions = notificationState.audioNotificationsOptions; + if ( + !audioOptions.enabled || + eventCount === 0 || + (playedFor !== null && playedFor >= eventCount) + ) { return; } - if (alertEventsCount === 0) { - return; + if (debounceTimeout) { + clearTimeout(debounceTimeout); } - if (playedFor.current && playedFor.current >= alertEventsCount) { - return; - } - - if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); - } - - const now = Date.now(); - const timeSinceLastPlay = now - lastPlayedTime.current; - const remainingDebounceTime = Math.max(0, timeout - timeSinceLastPlay); - debounceTimeout.current = setTimeout(() => { - playNotificationSound(audioNotificationsOptions.audioContext); - playedFor.current = alertEventsCount; - lastPlayedTime.current = Date.now(); - debounceTimeout.current = null; + const remainingDebounceTime = Math.max(0, AUDIO_DEBOUNCE_MS - (Date.now() - lastPlayedTime)); + debounceTimeout = setTimeout(() => { + playNotificationSound(audioOptions.audioContext); + playedFor = eventCount; + lastPlayedTime = Date.now(); + debounceTimeout = undefined; }, remainingDebounceTime); - }, [alertEventsCount]); + }); - useEffect(() => { - if (alertEventsCount !== 0) { - return; + createEffect(() => { + if (alertEventsCount() === 0) { + playedFor = null; } - playedFor.current = null; - }, [alertEventsCount]); + }); - useEffect(() => { - return () => { - if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); - } - }; - }, []); + onCleanup(() => { + if (debounceTimeout) { + clearTimeout(debounceTimeout); + } + }); return null; }; -export const NotificationWrapper = forwardRef((_, ref) => { +export const NotificationWrapper = (props: { ref?: JSX.HTMLAttributes["ref"] }) => { const events = useAppNotifications(); - const [notificationState, setNotificationState] = useState({ + const initialEvents = events(); + const [notificationState, setNotificationState] = createStore({ detailsExpanded: false, - events, + events: initialEvents, filterBy: "latest", moreInfoExpanded: false, route: "render-visualization", - selectedEvent: events.toSorted((a, b) => a.timestamp - b.timestamp).at(-1) ?? null, + selectedEvent: + initialEvents.toSorted((first, second) => first.timestamp - second.timestamp).at(-1) ?? null, selectedFiber: null, routeMessage: null, audioNotificationsOptions: { @@ -265,76 +253,61 @@ export const NotificationWrapper = forwardRef((_, ref) => { }, }); - notificationState.events = events; + createEffect(() => { + setNotificationState("events", events()); + }); + + const setRoute = ({ + route, + routeMessage, + }: { + route: NotificationsState["route"]; + routeMessage: NotificationsState["routeMessage"]; + }) => { + fadeOutHighlights(); + setNotificationState({ + route, + routeMessage, + selectedFiber: route === "render-explanation" ? notificationState.selectedFiber : null, + }); + }; + return ( { - setNotificationState((prev) => { - const newState = { ...prev, route, routeMessage }; - switch (route) { - case "render-visualization": { - fadeOutHighlights(); - return { - ...newState, - selectedFiber: null, - }; - } - case "optimize": { - fadeOutHighlights(); - return { - ...newState, - selectedFiber: null, - }; - } - case "other-visualization": { - fadeOutHighlights(); - return { - ...newState, - selectedFiber: null, - }; - } - case "render-explanation": { - // it would be ideal not to fade this out, but need to spend the time to sync the outline positions as they change in a performant (this was solved in react scan just need to follow same semantics) - fadeOutHighlights(); - - return newState; - } - } - route satisfies never; - }); - }, - }} + value={{ notificationState, setNotificationState, setRoute }} > - + ); -}); -const Notifications = forwardRef((_, ref) => { +}; + +const Notifications = (props: { ref?: JSX.HTMLAttributes["ref"] }) => { const { notificationState } = useNotificationsContext(); return ( -
- {notificationState.selectedEvent && ( -
- - {notificationState.moreInfoExpanded && } -
- )} +
+ + {(selectedEvent) => ( +
+ + + + +
+ )} +
((_, ref) => { "h-[calc(100%-150px)]", ])} > -
+
-
+
); -}); +}; const MoreInfo = () => { const { notificationState } = useNotificationsContext(); - - if (!notificationState.selectedEvent) { - throw new Error("Invariant must have selected event for more info"); - } - - const event = notificationState.selectedEvent; + const interactionEvent = createMemo(() => + notificationState.selectedEvent?.kind === "interaction" + ? notificationState.selectedEvent + : null, + ); return ( -
-
- {iife(() => { - switch (event.kind) { - case "interaction": { - return ( - <> -
- - {event.type === "click" - ? "Clicked component location" - : "Typed in component location"} - -
- {event.componentPath.toReversed().map((part, i) => ( - <> - - {part} - - {i < event.componentPath.length - 1 && ( - - )} - - ))} -
-
- -
- Total Time - - {getTotalTime(event.timing).toFixed(0)}ms - -
-
- Occurred - - {`${((Date.now() - event.timestamp) / 1000).toFixed(0)}s ago`} - -
- - ); - } - case "dropped-frames": { - return ( - <> -
- Total Time - - {getTotalTime(event.timing).toFixed(0)}ms - -
- -
- Occurred - - {`${((Date.now() - event.timestamp) / 1000).toFixed(0)}s ago`} - -
- - ); - } - } - })} -
-
+ + {(event) => ( +
+
+ +
+ + {interactionEvent()?.type === "click" + ? "Clicked component location" + : "Typed in component location"} + +
+ + {(part, index) => ( + <> + + {part} + + + + + + )} + +
+
+
+
+ Total Time + + {getTotalTime(event().timing).toFixed(0)}ms + +
+
+ Occurred + + {`${((Date.now() - event().timestamp) / 1000).toFixed(0)}s ago`} + +
+
+
+ )} +
); }; diff --git a/packages/scan/src/web/views/notifications/optimize.tsx b/packages/scan/src/web/views/notifications/optimize.tsx deleted file mode 100644 index ee4c2da8..00000000 --- a/packages/scan/src/web/views/notifications/optimize.tsx +++ /dev/null @@ -1,517 +0,0 @@ -import { useState } from "preact/hooks"; -import { cn } from "~web/utils/helpers"; -import { GroupedFiberRender, NotificationEvent, getComponentName, getTotalTime } from "./data"; -import { iife } from "~core/notifications/performance-utils"; - -const formatReactData = (groupedFiberRenders: Array) => { - let text = ""; - - const filteredFibers = groupedFiberRenders - .toSorted((a, b) => b.totalTime - a.totalTime) - .slice(0, 30) - .filter((fiber) => fiber.totalTime > 5); - - filteredFibers.forEach((fiberRender) => { - let localText = ""; - - localText += "Component Name:"; - localText += fiberRender.name; - localText += "\n"; - - localText += `Rendered: ${fiberRender.count} times\n`; - localText += `Sum of self times for ${fiberRender.name} is ${fiberRender.totalTime.toFixed(0)}ms\n`; - if (fiberRender.changes.props.length > 0) { - localText += `Changed props for all ${fiberRender.name} instances ("name:count" pairs)\n`; - fiberRender.changes.props.forEach((change) => { - localText += `${change.name}:${change.count}x\n`; - }); - } - - if (fiberRender.changes.state.length > 0) { - localText += `Changed state for all ${fiberRender.name} instances ("hook index:count" pairs)\n`; - fiberRender.changes.state.forEach((change) => { - localText += `${change.index}:${change.count}x\n`; - }); - } - - if (fiberRender.changes.context.length > 0) { - localText += `Changed context for all ${fiberRender.name} instances ("context display name (if exists):count" pairs)\n`; - fiberRender.changes.context.forEach((change) => { - localText += `${change.name}:${change.count}x\n`; - }); - } - - text += localText; - text += "\n"; - }); - - return text; -}; - -const generateInteractionDataPrompt = ({ - renderTime, - eHandlerTimeExcludingRenders, - toRafTime, - commitTime, - framePresentTime, - formattedReactData, -}: { - renderTime: number; - eHandlerTimeExcludingRenders: number; - toRafTime: number; - commitTime: number; - framePresentTime: number | null; - formattedReactData: string; -}) => { - return `I will provide you with a set of high level, and low level performance data about an interaction in a React App: -### High level -- react component render time: ${renderTime.toFixed(0)}ms -- how long it took to run javascript event handlers (EXCLUDING REACT RENDERS): ${eHandlerTimeExcludingRenders.toFixed(0)}ms -- how long it took from the last event handler time, to the last request animation frame: ${toRafTime.toFixed(0)}ms - - things like prepaint, style recalculations, layerization, async web API's like observers may occur during this time -- how long it took from the last request animation frame to when the dom was committed: ${commitTime.toFixed(0)}ms - - during this period you will see paint, commit, potential style recalcs, and other misc browser activity. Frequently high times here imply css that makes the browser do a lot of work, or mutating expensive dom properties during the event handler stage. This can be many things, but it narrows the problem scope significantly when this is high -${framePresentTime === null ? "" : `- how long it took from dom commit for the frame to be presented: ${framePresentTime.toFixed(0)}ms. This is when information about how to paint the next frame is sent to the compositor threads, and when the GPU does work. If this is high, look for issues that may be a bottleneck for operations occurring during this time`} - -### Low level -We also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered. -${formattedReactData}`; -}; - -const generateInteractionOptimizationPrompt = ({ - interactionType, - name, - componentPath, - time, - renderTime, - eHandlerTimeExcludingRenders, - toRafTime, - commitTime, - framePresentTime, - formattedReactData, -}: { - interactionType: string; - name: string; - componentPath: string; - - time: number; - renderTime: number; - eHandlerTimeExcludingRenders: number; - toRafTime: number; - commitTime: number; - framePresentTime: number | null; - formattedReactData: string; -}) => `You will attempt to implement a performance improvement to a user interaction in a React app. You will be provided with data about the interaction, and the slow down. - -Your should split your goals into 2 parts: -- identifying the problem -- fixing the problem - - it is okay to implement a fix even if you aren't 100% sure the fix solves the performance problem. When you aren't sure, you should tell the user to try repeating the interaction, and feeding the "Formatted Data" in the React Scan notifications optimize tab. This allows you to start a debugging flow with the user, where you attempt a fix, and observe the result. The user may make a mistake when they pass you the formatted data, so must make sure, given the data passed to you, that the associated data ties to the same interaction you were trying to debug. - - -Make sure to check if the user has the react compiler enabled (project dependent, configured through build tool), so you don't unnecessarily memoize components. If it is, you do not need to worry about memoizing user components - -One challenge you may face is the performance problem lies in a node_module, not in user code. If you are confident the problem originates because of a node_module, there are multiple strategies, which are context dependent: -- you can try to work around the problem, knowing which module is slow -- you can determine if its possible to resolve the problem in the node_module by modifying non node_module code -- you can monkey patch the node_module to experiment and see if it's really the problem (you can modify a functions properties to hijack the call for example) -- you can determine if it's feasible to replace whatever node_module is causing the problem with a performant option (this is an extreme) - -The interaction was a ${interactionType} on the component named ${name}. This component has the following ancestors ${componentPath}. This is the path from the component, to the root. This should be enough information to figure out where this component is in the user's code base - -This path is the component that was clicked, so it should tell you roughly where component had an event handler that triggered a state change. - -Please note that the leaf node of this path might not be user code (if they use a UI library), and they may contain many wrapper components that just pass through children that aren't relevant to the actual click. So make you sure analyze the path and understand what the user code is doing - -We have a set of high level, and low level data about the performance issue. - -The click took ${time.toFixed(0)}ms from interaction start, to when a new frame was presented to a user. - -We also provide you with a breakdown of what the browser spent time on during the period of interaction start to frame presentation. - -- react component render time: ${renderTime.toFixed(0)}ms -- how long it took to run javascript event handlers (EXCLUDING REACT RENDERS): ${eHandlerTimeExcludingRenders.toFixed(0)}ms -- how long it took from the last event handler time, to the last request animation frame: ${toRafTime.toFixed(0)}ms - - things like prepaint, style recalculations, layerization, async web API's like observers may occur during this time -- how long it took from the last request animation frame to when the dom was committed: ${commitTime.toFixed(0)}ms - - during this period you will see paint, commit, potential style recalcs, and other misc browser activity. Frequently high times here imply css that makes the browser do a lot of work, or mutating expensive dom properties during the event handler stage. This can be many things, but it narrows the problem scope significantly when this is high -${framePresentTime === null ? "" : `- how long it took from dom commit for the frame to be presented: ${framePresentTime.toFixed(0)}ms. This is when information about how to paint the next frame is sent to the compositor threads, and when the GPU does work. If this is high, look for issues that may be a bottleneck for operations occurring during this time`} - - -We also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered. - -${formattedReactData} - -You may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation - -It's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So the flow should be: -- find the most expensive components -- see what's causing them to render -- determine how you can make those state/props/context not change for a large set of the renders -- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. - -An important thing to note is that if you see a lot of react renders (some components with very high render counts), but javascript excluding renders is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run during the JS event handler period. - -It's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one. And it may also be the case the comparison of the hooks dependency can be expensive, and that would not be tracked in render time. - -If a node_module is the component with high renders, you can experiment to see if that component is the root issue (because of hooks). You should use the same instructions for node_module debugging mentioned previously. - -`; -const generateFrameDropOptimizationPrompt = ({ - renderTime, - otherTime, - formattedReactData, -}: { - renderTime: number; - - otherTime: number; - formattedReactData: string; -}) => `You will attempt to implement a performance improvement to a large slowdown in a react app - -Your should split your goals into 2 parts: -- identifying the problem -- fixing the problem - - it is okay to implement a fix even if you aren't 100% sure the fix solves the performance problem. When you aren't sure, you should tell the user to try repeating the interaction, and feeding the "Formatted Data" in the React Scan notifications optimize tab. This allows you to start a debugging flow with the user, where you attempt a fix, and observe the result. The user may make a mistake when they pass you the formatted data, so must make sure, given the data passed to you, that the associated data ties to the same interaction you were trying to debug. - -Make sure to check if the user has the react compiler enabled (project dependent, configured through build tool), so you don't unnecessarily memoize components. If it is, you do not need to worry about memoizing user components - -One challenge you may face is the performance problem lies in a node_module, not in user code. If you are confident the problem originates because of a node_module, there are multiple strategies, which are context dependent: -- you can try to work around the problem, knowing which module is slow -- you can determine if its possible to resolve the problem in the node_module by modifying non node_module code -- you can monkey patch the node_module to experiment and see if it's really the problem (you can modify a functions properties to hijack the call for example) -- you can determine if it's feasible to replace whatever node_module is causing the problem with a performant option (this is an extreme) - - -We have the high level time of how much react spent rendering, and what else the browser spent time on during this slowdown - -- react component render time: ${renderTime.toFixed(0)}ms -- other time: ${otherTime}ms - - -We also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered. - -${formattedReactData} - -You may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation - -It's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So the flow should be: -- find the most expensive components -- see what's causing them to render -- determine how you can make those state/props/context not change for a large set of the renders -- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. - -An important thing to note is that if you see a lot of react renders (some components with very high render counts), but other time is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run outside of what we profile (just react render time). - -It's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one. And it may also be the case the comparison of the hooks dependency can be expensive, and that would not be tracked in render time. - -If a node_module is the component with high renders, you can experiment to see if that component is the root issue (because of hooks). You should use the same instructions for node_module debugging mentioned previously. - -If renders don't seem to be the problem, see if there are any expensive CSS properties being added/mutated, or any expensive DOM Element mutations/new elements being created that could cause this slowdown. -`; - -const generateFrameDropExplanationPrompt = ({ - renderTime, - otherTime, - formattedReactData, -}: { - renderTime: number; - - otherTime: number; - formattedReactData: string; -}) => `Your goal will be to help me find the source of a performance problem in a React App. I collected a large dataset about this specific performance problem. - -We have the high level time of how much react spent rendering, and what else the browser spent time on during this slowdown - -- react component render time: ${renderTime.toFixed(0)}ms -- other time (other JavaScript, hooks like useEffect, style recalculations, layerization, paint & commit and everything else the browser might do to draw a new frame after javascript mutates the DOM): ${otherTime}ms - - -We also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered. - -${formattedReactData} - -You may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation - -It's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So a flow we can go through is: -- find the most expensive components -- see what's causing them to render -- determine how you can make those state/props/context not change for a large set of the renders -- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. - - -An important thing to note is that if you see a lot of react renders (some components with very high render counts), but other time is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run outside of what we profile (just react render time). - -It's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one, and this can add significant overhead when thousands of effects ran. - -If it's not possible to explain the root problem from this data, please ask me for more data explicitly, and what we would need to know to find the source of the performance problem. -`; - -const generateFrameDropDataPrompt = ({ - renderTime, - otherTime, - formattedReactData, -}: { - renderTime: number; - - otherTime: number; - formattedReactData: string; -}) => `I will provide you with a set of high level, and low level performance data about a large frame drop in a React App: -### High level -- react component render time: ${renderTime.toFixed(0)}ms -- how long it took to run everything else (other JavaScript, hooks like useEffect, style recalculations, layerization, paint & commit and everything else the browser might do to draw a new frame after javascript mutates the DOM): ${otherTime}ms - -### Low level -We also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered. -${formattedReactData}`; - -const generateInteractionExplanationPrompt = ({ - interactionType, - name, - time, - renderTime, - eHandlerTimeExcludingRenders, - toRafTime, - commitTime, - framePresentTime, - formattedReactData, -}: { - interactionType: string; - name: string; - time: number; - renderTime: number; - eHandlerTimeExcludingRenders: number; - toRafTime: number; - commitTime: number; - framePresentTime: number | null; - formattedReactData: string; -}) => `Your goal will be to help me find the source of a performance problem. I collected a large dataset about this specific performance problem. - -There was a ${interactionType} on a component named ${name}. This means, roughly, the component that handled the ${interactionType} event was named ${name}. - -We have a set of high level, and low level data about the performance issue. - -The click took ${time.toFixed(0)}ms from interaction start, to when a new frame was presented to a user. - -We also provide you with a breakdown of what the browser spent time on during the period of interaction start to frame presentation. - -- react component render time: ${renderTime.toFixed(0)}ms -- how long it took to run javascript event handlers (EXCLUDING REACT RENDERS): ${eHandlerTimeExcludingRenders.toFixed(0)}ms -- how long it took from the last event handler time, to the last request animation frame: ${toRafTime.toFixed(0)}ms - - things like prepaint, style recalculations, layerization, async web API's like observers may occur during this time -- how long it took from the last request animation frame to when the dom was committed: ${commitTime.toFixed(0)}ms - - during this period you will see paint, commit, potential style recalcs, and other misc browser activity. Frequently high times here imply css that makes the browser do a lot of work, or mutating expensive dom properties during the event handler stage. This can be many things, but it narrows the problem scope significantly when this is high -${framePresentTime === null ? "" : `- how long it took from dom commit for the frame to be presented: ${framePresentTime.toFixed(0)}ms. This is when information about how to paint the next frame is sent to the compositor threads, and when the GPU does work. If this is high, look for issues that may be a bottleneck for operations occurring during this time`} - -We also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered. - -${formattedReactData} - - -You may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation - -It's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So a flow we can go through is: -- find the most expensive components -- see what's causing them to render -- determine how you can make those state/props/context not change for a large set of the renders -- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. - - -An important thing to note is that if you see a lot of react renders (some components with very high render counts), but javascript excluding renders is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run during the JS event handler period. - -It's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one. And it may also be the case the comparison of the hooks dependency can be expensive, and that would not be tracked in render time. - -If it's not possible to explain the root problem from this data, please ask me for more data explicitly, and what we would need to know to find the source of the performance problem. -`; -export const getLLMPrompt = ( - activeTab: "fix" | "data" | "explanation", - selectedEvent: NotificationEvent, -) => - iife(() => { - switch (activeTab) { - case "data": { - switch (selectedEvent.kind) { - case "dropped-frames": { - return generateFrameDropDataPrompt({ - formattedReactData: formatReactData(selectedEvent.groupedFiberRenders), - renderTime: selectedEvent.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.totalTime, - 0, - ), - otherTime: selectedEvent.timing.otherTime, - }); - } - case "interaction": { - return generateInteractionDataPrompt({ - commitTime: selectedEvent.timing.frameConstruction, - eHandlerTimeExcludingRenders: selectedEvent.timing.otherJSTime, - formattedReactData: formatReactData(selectedEvent.groupedFiberRenders), - framePresentTime: selectedEvent.timing.frameDraw, - renderTime: selectedEvent.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.totalTime, - 0, - ), - toRafTime: selectedEvent.timing.framePreparation, - }); - } - } - } - case "explanation": { - switch (selectedEvent.kind) { - case "dropped-frames": { - return generateFrameDropExplanationPrompt({ - formattedReactData: formatReactData(selectedEvent.groupedFiberRenders), - renderTime: selectedEvent.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.totalTime, - 0, - ), - otherTime: selectedEvent.timing.otherTime, - }); - } - case "interaction": { - return generateInteractionExplanationPrompt({ - commitTime: selectedEvent.timing.frameConstruction, - eHandlerTimeExcludingRenders: selectedEvent.timing.otherJSTime, - formattedReactData: formatReactData(selectedEvent.groupedFiberRenders), - framePresentTime: selectedEvent.timing.frameDraw, - interactionType: selectedEvent.type, - name: getComponentName(selectedEvent.componentPath), - renderTime: selectedEvent.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.totalTime, - 0, - ), - time: getTotalTime(selectedEvent.timing), - toRafTime: selectedEvent.timing.framePreparation, - }); - } - } - } - case "fix": { - switch (selectedEvent.kind) { - case "dropped-frames": { - return generateFrameDropOptimizationPrompt({ - formattedReactData: formatReactData(selectedEvent.groupedFiberRenders), - - renderTime: selectedEvent.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.totalTime, - 0, - ), - otherTime: selectedEvent.timing.otherTime, - }); - } - case "interaction": { - return generateInteractionOptimizationPrompt({ - commitTime: selectedEvent.timing.frameConstruction, - componentPath: selectedEvent.componentPath.join(">"), - eHandlerTimeExcludingRenders: selectedEvent.timing.otherJSTime, - formattedReactData: formatReactData(selectedEvent.groupedFiberRenders), - framePresentTime: selectedEvent.timing.frameDraw, - interactionType: selectedEvent.type, - name: getComponentName(selectedEvent.componentPath), - renderTime: selectedEvent.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.totalTime, - 0, - ), - time: getTotalTime(selectedEvent.timing), - toRafTime: selectedEvent.timing.framePreparation, - }); - } - } - } - } - }); - -export const Optimize = ({ selectedEvent }: { selectedEvent: NotificationEvent }) => { - const [activeTab, setActiveTab] = useState<"fix" | "explanation" | "data">("fix"); - const [copying, setCopying] = useState(false); - - return ( -
-
-
-
- - - - -
-
-
-
-            {getLLMPrompt(activeTab, selectedEvent)}
-          
-
-
- -
- ); -}; diff --git a/packages/scan/src/web/views/notifications/other-visualization.tsx b/packages/scan/src/web/views/notifications/other-visualization.tsx index 311329fc..74d8570b 100644 --- a/packages/scan/src/web/views/notifications/other-visualization.tsx +++ b/packages/scan/src/web/views/notifications/other-visualization.tsx @@ -1,11 +1,9 @@ -import { ReactNode } from "preact/compat"; -import { useContext, useEffect, useState } from "preact/hooks"; -import { getIsProduction } from "~core/index"; -import { iife } from "~core/notifications/performance-utils"; -import { cn } from "~web/utils/helpers"; +import { For, Show, createEffect, createMemo, createSignal, useContext } from "solid-js"; +import { getIsProduction } from "../../../core/index"; +import { iife } from "../../../core/notifications/performance-utils"; +import { cn } from "../../utils/helpers"; import { InteractionEvent, NotificationEvent, getTotalTime, useNotificationsContext } from "./data"; -import { getLLMPrompt } from "./optimize"; -import { ToolbarElementContext } from "~web/widget"; +import { ToolbarElementContext } from "../../widget"; type BaseTimeDataItem = { name: string; time: number; @@ -87,171 +85,176 @@ const getTimeData = (selectedEvent: NotificationEvent, isProduction: boolean) => } }; -export const OtherVisualization = ({ selectedEvent }: { selectedEvent: NotificationEvent }) => { - const [isProduction] = useState(getIsProduction() ?? false); +export const OtherVisualization = (props: { selectedEvent: NotificationEvent }) => { + const isProduction = getIsProduction() ?? false; const { notificationState } = useNotificationsContext(); - const [expandedItems, setExpandedItems] = useState( + const [expandedItems, setExpandedItems] = createSignal( notificationState.routeMessage?.name ? [notificationState.routeMessage.name] : [], ); - const timeData = getTimeData(selectedEvent, isProduction); + const timeData = createMemo(() => getTimeData(props.selectedEvent, isProduction)); const root = useContext(ToolbarElementContext); // for when a user clicks a bar of a non render, and gets sent to the other visualization and passes a route message on the way - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { + createEffect(() => { + if (notificationState.route !== "other-visualization") return; if (notificationState.routeMessage?.name) { const container = root?.querySelector("#overview-scroll-container"); const element = root?.querySelector( `#react-scan-overview-bar-${notificationState.routeMessage.name}`, - ) as HTMLElement; + ); - if (container && element) { + if (container instanceof HTMLElement && element instanceof HTMLElement) { const elementTop = element.getBoundingClientRect().top; const containerTop = container.getBoundingClientRect().top; const scrollOffset = elementTop - containerTop; container.scrollTop = container.scrollTop + scrollOffset; } } - }, [notificationState.route]); + }); - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { + createEffect(() => { if (notificationState.route === "other-visualization") { - setExpandedItems((prev) => - notificationState.routeMessage?.name ? [notificationState.routeMessage.name] : prev, + setExpandedItems((previousItems) => + notificationState.routeMessage?.name + ? [notificationState.routeMessage.name] + : previousItems, ); } - }, [notificationState.route]); + }); - const totalTime = timeData.reduce((acc, item) => acc + item.time, 0); + const totalTime = createMemo(() => timeData().reduce((total, item) => total + item.time, 0)); return ( -
-
-
-

What was time spent on?

- Total: {totalTime.toFixed(0)}ms +
+
+
+

What was time spent on?

+ Total: {totalTime().toFixed(0)}ms
-
- {timeData.map((entry) => { - const isExpanded = expandedItems.includes(entry.kind); - return ( -
- - {isExpanded && ( -
-

- {iife(() => { - switch (selectedEvent.kind) { - case "interaction": { - switch (entry.kind) { - case "render": { - return ; - } - - case "other-javascript": { - return ; - } - - case "other-not-javascript": { - return ; + + +

+

+ {iife(() => { + const selectedEvent = props.selectedEvent; + switch (selectedEvent.kind) { + case "interaction": { + switch (entry.kind) { + case "render": { + return ; + } + + case "other-javascript": { + return ; + } + + case "other-not-javascript": { + return ; + } } } - } - case "dropped-frames": { - switch (entry.kind) { - case "total-processing-time": { - return ( - - ); - } - case "render": { - return ( - <> + case "dropped-frames": { + switch (entry.kind) { + case "total-processing-time": { + return ( b.totalTime - a.totalTime) - .slice(0, 3) - .map((render) => ({ - name: render.name, - percentage: - render.totalTime / getTotalTime(selectedEvent.timing), - })), + time: getTotalTime(selectedEvent.timing), }, }} /> - - ); - } - case "other-frame-drop": { - return ( - - ); + ); + } + case "render": { + return ( + <> + b.totalTime - a.totalTime) + .slice(0, 3) + .map((render) => ({ + name: render.name, + percentage: + render.totalTime / + getTotalTime(selectedEvent.timing), + })), + }, + }} + /> + + ); + } + case "other-frame-drop": { + return ( + + ); + } } } } - } - })} -

-
- )} -
- ); - })} + })} +

+
+ +
+ ); + }} +
); @@ -285,14 +288,10 @@ type OverviewInput = data: { count: number; percentageOfTotal: number; - copyButton: ReactNode; }; } | { kind: "update-dom-draw-frame"; - data: { - copyButton: ReactNode; - }; } | { kind: "render"; @@ -315,64 +314,15 @@ const getDrawInput = (event: InteractionEvent): OverviewInput => { data: { count: renderCount, percentageOfTotal: renderPercentage, - copyButton: , }, }; } return { kind: "update-dom-draw-frame", - data: { - copyButton: , - }, }; }; -const CopyPromptButton = () => { - const [copying, setCopying] = useState(false); - const { notificationState } = useNotificationsContext(); - - return ( - - ); -}; - const getRenderInput = (event: InteractionEvent): OverviewInput => { if (event.timing.renderTime / getTotalTime(event.timing) > 0.3) { return { @@ -437,230 +387,219 @@ const getJSInput = (event: InteractionEvent): OverviewInput => { }; }; -const Explanation = ({ input }: { input: OverviewInput }) => { - switch (input.kind) { - case "total-processing": { - return ( -
-

- This is the time it took to draw the entire frame that was presented to the user. To be - at 60FPS, this number needs to be {"<=16ms"} -

- -

- To debug the issue, check the "Ranked" tab to see if there are significant component - renders -

-

- On a production React build, React Scan can't access the time it took for component to - render. To get that information, run React Scan on a development build -

- -

- To understand precisely what caused the slowdown while in production, use the{" "} - Chrome profiler and analyze the function call times. -

- -

-
- ); - } - case "render": { - return ( -
-

- This is the time it took React to run components, and internal logic to handle the - output of your component. -

- -
-

The slowest components for this time period were:

- {input.data.topByTime.map((item) => ( -
- {item.name}: {(item.percentage * 100).toFixed(0)}% of total -
- ))} +const Explanation = (props: { input: OverviewInput }) => { + const content = createMemo(() => { + const input = props.input; + switch (input.kind) { + case "total-processing": { + return ( +
+

+ This is the time it took to draw the entire frame that was presented to the user. To + be at 60FPS, this number needs to be {"<=16ms"} +

+ +

+ To debug the issue, check the "Ranked" tab to see if there are significant component + renders +

+

+ On a production React build, React Scan can't access the time it took for component to + render. To get that information, run React Scan on a development build +

+ +

+ To understand precisely what caused the slowdown while in production, use the{" "} + Chrome profiler and analyze the function call times. +

+ +

-

- To view the render times of all your components, and what caused them to render, go to - the "Ranked" tab -

-

The "Ranked" tab shows the render times of every component.

-

The render times of the same components are grouped together into one bar.

-

- Clicking the component will show you what props, state, or context caused the component - to re-render. -

-
- ); - } - case "js-explanation-base": { - return ( -
-

- This is the period when JavaScript hooks and other JavaScript outside of React Renders - run. -

-

- The most common culprit for high JS time is expensive hooks, like expensive callbacks - inside of useEffect's or a large number of useEffect's called, but this can - also be JavaScript event handlers ('onclick', 'onchange') that - performed expensive computation. -

-

- If you have lots of components rendering that call hooks, like useEffect, it can add - significant overhead even if the callbacks are not expensive. If this is the case, you - can try optimizing the renders of those components to avoid the hook from having to run. -

-

- You should profile your app using the Chrome DevTools profiler to learn - exactly which functions took the longest to execute. -

-
- ); - } - case "high-render-count-high-js": { - return ( -
-

- This is the period when JavaScript hooks and other JavaScript outside of React Renders - run. -

- {input.data.renderCount === 0 ? ( - <> -

- There were no renders, which means nothing related to React caused this slowdown. - The most likely cause of the slowdown is a slow JavaScript event handler, or code - related to a Web API -

-

- You should try to reproduce the slowdown while profiling your website with the - Chrome DevTools profiler to see exactly what functions took the - longest to execute. -

- - ) : ( - <> - {" "} -

- There were {input.data.renderCount} renders, which could have - contributed to the high JavaScript/Hook time if they ran lots of hooks, like{" "} - useEffects. -

-
-

You should try optimizing the renders of:

- {input.data.topByCount.map((item) => ( -
- - {item.name} (rendered {item.count}x) + ); + } + case "render": { + return ( +
+

+ This is the time it took React to run components, and internal logic to handle the + output of your component. +

+ +
+

The slowest components for this time period were:

+ + {(item) => ( +
+ {item.name}: {(item.percentage * 100).toFixed(0)}% of total
- ))} -
- and then checking if the problem still exists. -

- You can also try profiling your app using the{" "} - Chrome DevTools profiler to see exactly what functions took the - longest to execute. -

- - )} -
- ); - } - case "low-render-count-high-js": { - return ( -
-

- This is the period when JavaScript hooks and other JavaScript outside of React Renders - run. -

-

- There were only {input.data.renderCount} renders detected, which means - either you had very expensive hooks like useEffect/ - useLayoutEffect, or there is other JavaScript running during this - interaction that took up the majority of the time. -

-

- To understand precisely what caused the slowdown, use the{" "} - Chrome profiler and analyze the function call times. -

-
- ); - } - case "high-render-count-update-dom-draw-frame": { - return ( -
-

- These are the calculations the browser is forced to do in response to the JavaScript - that ran during the interaction. -

-

- This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations. -

-

- During this interaction, there were {input.data.count} renders, which - was {input.data.percentageOfTotal.toFixed(0)}% of the time spent - processing -

-

- The work performed as a result of the renders may have forced the browser to spend a lot - of time to draw the next frame. -

-

- You can try optimizing the renders to see if the performance problem still exists using - the "Ranked" tab. -

-

- If you use an AI-based code editor, you can export the performance data collected as a - prompt. -

- -

{input.data.copyButton}

-

- Provide this formatted data to the model and ask it to find, or fix, what could be - causing this performance problem. -

-

For a larger selection of prompts, try the "Prompts" tab

-
- ); - } - case "update-dom-draw-frame": { - return ( -
-

- These are the calculations the browser is forced to do in response to the JavaScript - that ran during the interaction. -

-

- This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations. -

-

- If you use an AI-based code editor, you can export the performance data collected as a - prompt. -

- -

{input.data.copyButton}

-

- Provide this formatted data to the model and ask it to find, or fix, what could be - causing this performance problem. -

-

For a larger selection of prompts, try the "Prompts" tab

-
- ); - } - case "other": { - return ( -
-

- This is the time it took to run everything other than React renders. This can be hooks - like useEffect, other JavaScript not part of React, or work the browser has - to do to update the DOM and draw the next frame. -

-

- To get a better picture of what happened, profile your app using the{" "} - Chrome profiler when the performance problem arises. -

-
- ); + )} + +
+

+ To view the render times of all your components, and what caused them to render, go to + the "Ranked" tab +

+

The "Ranked" tab shows the render times of every component.

+

The render times of the same components are grouped together into one bar.

+

+ Clicking the component will show you what props, state, or context caused the + component to re-render. +

+
+ ); + } + case "js-explanation-base": { + return ( +
+

+ This is the period when JavaScript hooks and other JavaScript outside of React Renders + run. +

+

+ The most common culprit for high JS time is expensive hooks, like expensive callbacks + inside of useEffect's or a large number of useEffect's called, but this + can also be JavaScript event handlers ('onclick', 'onchange' + ) that performed expensive computation. +

+

+ If you have lots of components rendering that call hooks, like useEffect, it can add + significant overhead even if the callbacks are not expensive. If this is the case, you + can try optimizing the renders of those components to avoid the hook from having to + run. +

+

+ You should profile your app using the Chrome DevTools profiler to + learn exactly which functions took the longest to execute. +

+
+ ); + } + case "high-render-count-high-js": { + return ( +
+

+ This is the period when JavaScript hooks and other JavaScript outside of React Renders + run. +

+ {input.data.renderCount === 0 ? ( + <> +

+ There were no renders, which means nothing related to React caused this slowdown. + The most likely cause of the slowdown is a slow JavaScript event handler, or code + related to a Web API +

+

+ You should try to reproduce the slowdown while profiling your website with the + Chrome DevTools profiler to see exactly what functions took the + longest to execute. +

+ + ) : ( + <> + {" "} +

+ There were {input.data.renderCount} renders, which could have + contributed to the high JavaScript/Hook time if they ran lots of hooks, like{" "} + useEffects. +

+
+

You should try optimizing the renders of:

+ + {(item) => ( +
+ - {item.name} (rendered {item.count}x) +
+ )} +
+
+ and then checking if the problem still exists. +

+ You can also try profiling your app using the{" "} + Chrome DevTools profiler to see exactly what functions took the + longest to execute. +

+ + )} +
+ ); + } + case "low-render-count-high-js": { + return ( +
+

+ This is the period when JavaScript hooks and other JavaScript outside of React Renders + run. +

+

+ There were only {input.data.renderCount} renders detected, which + means either you had very expensive hooks like useEffect/ + useLayoutEffect, or there is other JavaScript running during this + interaction that took up the majority of the time. +

+

+ To understand precisely what caused the slowdown, use the{" "} + Chrome profiler and analyze the function call times. +

+
+ ); + } + case "high-render-count-update-dom-draw-frame": { + return ( +
+

+ These are the calculations the browser is forced to do in response to the JavaScript + that ran during the interaction. +

+

+ This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM + mutations. +

+

+ During this interaction, there were {input.data.count} renders, which + was {input.data.percentageOfTotal.toFixed(0)}% of the time spent + processing +

+

+ The work performed as a result of the renders may have forced the browser to spend a + lot of time to draw the next frame. +

+

+ You can try optimizing the renders to see if the performance problem still exists + using the "Ranked" tab. +

+
+ ); + } + case "update-dom-draw-frame": { + return ( +
+

+ These are the calculations the browser is forced to do in response to the JavaScript + that ran during the interaction. +

+

+ This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM + mutations. +

+
+ ); + } + case "other": { + return ( +
+

+ This is the time it took to run everything other than React renders. This can be hooks + like useEffect, other JavaScript not part of React, or work the browser + has to do to update the DOM and draw the next frame. +

+

+ To get a better picture of what happened, profile your app using the{" "} + Chrome profiler when the performance problem arises. +

+
+ ); + } } - } + }); + return <>{content()}; }; diff --git a/packages/scan/src/web/views/notifications/popover.tsx b/packages/scan/src/web/views/notifications/popover.tsx index 354cef17..3a058035 100644 --- a/packages/scan/src/web/views/notifications/popover.tsx +++ b/packages/scan/src/web/views/notifications/popover.tsx @@ -1,42 +1,52 @@ import { - ComponentProps, - ReactNode, - createPortal, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, useContext, - useEffect, - useRef, - useState, -} from 'preact/compat'; -import { cn } from '~web/utils/helpers'; -import { ToolbarElementContext } from '~web/widget'; - -type PopoverState = 'closed' | 'opening' | 'open' | 'closing'; - -/** - * - * fixme: very hacky and suboptimal popover (api and implementation) - */ -export const Popover = ({ - children, - triggerContent, - wrapperProps, -}: { - children: ReactNode; - triggerContent: ReactNode; - wrapperProps?: ComponentProps<'div'>; + type JSX, +} from "solid-js"; +import { Portal } from "solid-js/web"; +import { cn } from "../../utils/helpers"; +import { ToolbarElementContext } from "../../widget"; + +type PopoverState = "closed" | "opening" | "open" | "closing"; + +export const Popover = (props: { + children: JSX.Element; + triggerContent: JSX.Element; + wrapperProps?: JSX.HTMLAttributes; }) => { - const [popoverState, setPopoverState] = useState('closed'); - const [elBoundingRect, setElBoundingRect] = useState(null); - const [viewportSize, setViewportSize] = useState({ + const [popoverState, setPopoverState] = createSignal("closed"); + const [elementBoundingRect, setElementBoundingRect] = createSignal(); + const [viewportSize, setViewportSize] = createSignal({ width: window.innerWidth, height: window.innerHeight, }); - const triggerRef = useRef(null); - const popoverRef = useRef(null); - const portalEl = useContext(ToolbarElementContext); - const isHoveredRef = useRef(false); + const portalElement = useContext(ToolbarElementContext); + let triggerElement: HTMLDivElement | undefined; + let popoverElement: HTMLDivElement | undefined; + let isHovered = false; - useEffect(() => { + const updateRect = () => { + if (!triggerElement || !portalElement) return; + + const triggerRect = triggerElement.getBoundingClientRect(); + const portalRect = portalElement.getBoundingClientRect(); + setElementBoundingRect( + new DOMRect( + triggerRect.left + triggerRect.width / 2 - portalRect.left, + triggerRect.top - portalRect.top, + triggerRect.width, + triggerRect.height, + ), + ); + }; + + onMount(() => { + updateRect(); const handleResize = () => { setViewportSize({ width: window.innerWidth, @@ -44,141 +54,119 @@ export const Popover = ({ }); updateRect(); }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - const updateRect = () => { - if (triggerRef.current && portalEl) { - const triggerRect = triggerRef.current.getBoundingClientRect(); - const portalRect = portalEl.getBoundingClientRect(); - - const centerX = triggerRect.left + triggerRect.width / 2; - const centerY = triggerRect.top; - - const rect = new DOMRect( - centerX - portalRect.left, - centerY - portalRect.top, - triggerRect.width, - triggerRect.height, - ); - setElBoundingRect(rect); - } - }; - - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - updateRect(); - }, [triggerRef.current]); - - useEffect(() => { - if (popoverState === 'opening') { - const timer = setTimeout(() => setPopoverState('open'), 120); - return () => clearTimeout(timer); - } else if (popoverState === 'closing') { - const timer = setTimeout(() => setPopoverState('closed'), 120); - return () => clearTimeout(timer); - } - }, [popoverState]); - - // just incase we didn't capture the mouse leave event because the underlying container moved - useEffect(() => { - const interval = setInterval(() => { - if (!isHoveredRef.current && popoverState !== 'closed') { - setPopoverState('closing'); + const hoverCheckInterval = setInterval(() => { + if (!isHovered && popoverState() !== "closed") { + setPopoverState("closing"); } }, 1000); - return () => clearInterval(interval); - }, [popoverState]); + window.addEventListener("resize", handleResize); + onCleanup(() => { + window.removeEventListener("resize", handleResize); + clearInterval(hoverCheckInterval); + }); + }); - const handleMouseEnter = () => { - isHoveredRef.current = true; - updateRect(); - setPopoverState('opening'); - }; + createEffect(() => { + const currentState = popoverState(); + if (currentState !== "opening" && currentState !== "closing") return; - const handleMouseLeave = () => { - isHoveredRef.current = false; - updateRect(); - setPopoverState('closing'); - }; + const timer = setTimeout( + () => setPopoverState(currentState === "opening" ? "open" : "closed"), + 120, + ); + onCleanup(() => clearTimeout(timer)); + }); - const getPopoverPosition = () => { - if (!elBoundingRect || !portalEl) return { top: 0, left: 0 }; + const popoverPosition = createMemo(() => { + const boundingRect = elementBoundingRect(); + if (!boundingRect || !portalElement) return { top: 0, left: 0 }; - const portalRect = portalEl.getBoundingClientRect(); + const portalRect = portalElement.getBoundingClientRect(); const popoverWidth = 175; - const popoverHeight = popoverRef.current?.offsetHeight || 40; + const popoverHeight = popoverElement?.offsetHeight || 40; const safeArea = 5; - - const viewportX = elBoundingRect.x + portalRect.left; - const viewportY = elBoundingRect.y + portalRect.top; - + const viewportX = boundingRect.x + portalRect.left; + const viewportY = boundingRect.y + portalRect.top; let left = viewportX; let top = viewportY - 4; if (left - popoverWidth / 2 < safeArea) { left = safeArea + popoverWidth / 2; - } else if (left + popoverWidth / 2 > viewportSize.width - safeArea) { - left = viewportSize.width - safeArea - popoverWidth / 2; + } else if (left + popoverWidth / 2 > viewportSize().width - safeArea) { + left = viewportSize().width - safeArea - popoverWidth / 2; } if (top - popoverHeight < safeArea) { - top = viewportY + elBoundingRect.height + 4; + top = viewportY + boundingRect.height + 4; } return { top: top - portalRect.top, left: left - portalRect.left, }; + }); + + const handleMouseEnter = () => { + isHovered = true; + updateRect(); + setPopoverState("opening"); }; - const popoverPosition = getPopoverPosition(); + const handleMouseLeave = () => { + isHovered = false; + updateRect(); + setPopoverState("closing"); + }; return ( <> - {portalEl && - elBoundingRect && - popoverState !== 'closed' && - createPortal( -
- {children} -
, - portalEl, + + {(mountElement) => ( + +
+ {props.children} +
+
)} +
- {triggerContent} + {props.triggerContent}
); diff --git a/packages/scan/src/web/views/notifications/render-bar-chart.tsx b/packages/scan/src/web/views/notifications/render-bar-chart.tsx index 248d804e..e62aa39f 100644 --- a/packages/scan/src/web/views/notifications/render-bar-chart.tsx +++ b/packages/scan/src/web/views/notifications/render-bar-chart.tsx @@ -1,8 +1,8 @@ -import { useRef, useState } from "preact/hooks"; -import { getBatchedRectMap } from "src/new-outlines"; -import { getIsProduction } from "~core/index"; -import { iife } from "~core/notifications/performance-utils"; -import { cn } from "~web/utils/helpers"; +import { For, Show, createMemo, createSignal } from "solid-js"; +import { getBatchedRectMap } from "../../../new-outlines"; +import { getIsProduction } from "../../../core/index"; +import { iife } from "../../../core/notifications/performance-utils"; +import { cn } from "../../utils/helpers"; import { GroupedFiberRender, NotificationEvent, @@ -10,41 +10,46 @@ import { isRenderMemoizable, useNotificationsContext, } from "./data"; -import { HighlightStore, drawHighlights } from "~core/notifications/outline-overlay"; +import { + drawHighlights, + getHighlightState, + setHighlightState, +} from "../../../core/notifications/outline-overlay"; import { ChevronRight } from "./icons"; // todo: cleanup, convoluted ternaries export const fadeOutHighlights = () => { - const curr = HighlightStore.value.current - ? HighlightStore.value.current - : HighlightStore.value.kind === "transition" - ? HighlightStore.value.transitionTo + const state = getHighlightState(); + const curr = state.current + ? state.current + : state.kind === "transition" + ? state.transitionTo : null; if (!curr) { return; } - if (HighlightStore.value.kind === "transition") { - HighlightStore.value = { + if (state.kind === "transition") { + setHighlightState({ kind: "move-out", // because we want to dynamically fade this value current: - HighlightStore.value.current?.alpha === 0 + state.current?.alpha === 0 ? // we want to only start fading from transition if current is done animating out - HighlightStore.value.transitionTo + state.transitionTo : // if current doesn't exist then transition must exist - (HighlightStore.value.current ?? HighlightStore.value.transitionTo), - }; + (state.current ?? state.transitionTo), + }); return; } - HighlightStore.value = { + setHighlightState({ kind: "move-out", current: { alpha: 0, ...curr, }, - }; + }); }; type Bars = Array< @@ -54,101 +59,94 @@ type Bars = Array< | { kind: "render"; event: GroupedFiberRender; totalTime: number } >; -export const RenderBarChart = ({ selectedEvent }: { selectedEvent: NotificationEvent }) => { - const totalInteractionTime = getTotalTime(selectedEvent.timing); - const nonRender = totalInteractionTime - selectedEvent.timing.renderTime; - const [isProduction] = useState(getIsProduction()); - const events = selectedEvent.groupedFiberRenders; - const bars: Bars = events.map((event) => ({ - event, - kind: "render", - totalTime: isProduction ? event.count : event.totalTime, - })); - - const isShowingExtraInfo = iife(() => { - switch (selectedEvent.kind) { - case "dropped-frames": { - return selectedEvent.timing.renderTime / totalInteractionTime < 0.1; - } - case "interaction": { - return ( - (selectedEvent.timing.otherJSTime + selectedEvent.timing.renderTime) / +export const RenderBarChart = (props: { selectedEvent: NotificationEvent }) => { + const isProduction = getIsProduction(); + const bars = createMemo(() => { + const selectedEvent = props.selectedEvent; + const totalInteractionTime = getTotalTime(selectedEvent.timing); + const result: Bars = selectedEvent.groupedFiberRenders.map((event) => ({ + event, + kind: "render", + totalTime: isProduction ? event.count : event.totalTime, + })); + const isShowingExtraInfo = + selectedEvent.kind === "dropped-frames" + ? selectedEvent.timing.renderTime / totalInteractionTime < 0.1 + : (selectedEvent.timing.otherJSTime + selectedEvent.timing.renderTime) / totalInteractionTime < - 0.2 - ); - } - } - }); - /** - * We don't add the extra bars in production because we can't compare them to the renders, so the bar is useless, user can use overview tab to see times - */ - if (selectedEvent.kind === "interaction" && !isProduction) { - bars.push({ - kind: "other-javascript", - totalTime: selectedEvent.timing.otherJSTime, - }); - } + 0.2; - if (isShowingExtraInfo && !isProduction) { - if (selectedEvent.kind === "interaction") { - bars.push({ - kind: "other-not-javascript", - totalTime: - getTotalTime(selectedEvent.timing) - - selectedEvent.timing.renderTime - - selectedEvent.timing.otherJSTime, - }); - } else { - bars.push({ - kind: "other-frame-drop", - totalTime: nonRender, + if (selectedEvent.kind === "interaction" && !isProduction) { + result.push({ + kind: "other-javascript", + totalTime: selectedEvent.timing.otherJSTime, }); } - } + if (isShowingExtraInfo && !isProduction) { + if (selectedEvent.kind === "interaction") { + result.push({ + kind: "other-not-javascript", + totalTime: + totalInteractionTime - + selectedEvent.timing.renderTime - + selectedEvent.timing.otherJSTime, + }); + } else { + result.push({ + kind: "other-frame-drop", + totalTime: totalInteractionTime - selectedEvent.timing.renderTime, + }); + } + } + return result; + }); - const debouncedMouseEnter = useRef<{ + const debouncedMouseEnter: { timer: ReturnType | null; lastCallAt: number | null; - }>({ + } = { lastCallAt: null, timer: null, - }); + }; - const totalBarTime = bars.reduce((prev, curr) => prev + curr.totalTime, 0); + const totalBarTime = createMemo(() => + bars().reduce((totalTime, bar) => totalTime + bar.totalTime, 0), + ); return ( -
+
{iife(() => { - if (isProduction && bars.length === 0) { + if (isProduction && bars().length === 0) { return ( -
-

No data available

-

No data was collected during this period

+
+

No data available

+

No data was collected during this period

); } - if (bars.length === 0) { + if (bars().length === 0) { return ( -
-

No renders collected

-

There were no renders during this period

+
+

No renders collected

+

There were no renders during this period

); } })} - {bars - .toSorted((a, b) => b.totalTime - a.totalTime) - .map((bar) => ( + secondBar.totalTime - firstBar.totalTime)} + > + {(bar) => ( - ))} + )} +
); }; @@ -178,16 +176,14 @@ const RenderBar = ({ bars: Bars; bar: Bars[number]; debouncedMouseEnter: { - current: { - timer: ReturnType | null; - lastCallAt: number | null; - }; + timer: ReturnType | null; + lastCallAt: number | null; }; totalBarTime: number; isProduction: boolean | null; }) => { const { setNotificationState, setRoute } = useNotificationsContext(); - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = createSignal(false); const isLeaf = bar.kind === "render" ? bar.event.parents.size === 0 : true; @@ -206,10 +202,7 @@ const RenderBar = ({ const handleBarClick = () => { if (bar.kind === "render") { - setNotificationState((prev) => ({ - ...prev, - selectedFiber: bar.event, - })); + setNotificationState("selectedFiber", bar.event); setRoute({ route: "render-explanation", @@ -227,42 +220,43 @@ const RenderBar = ({ }; return ( -
-
+
+
-
-
-
- {selectedFiber.name} -
+
+
+
{props.selectedFiber.name}
-
+
{!isProduction && ( <> -
- • Render time: {selectedFiber.totalTime.toFixed(0)}ms +
+ • Render time: {props.selectedFiber.totalTime.toFixed(0)}ms
)} -
- • Renders: {selectedFiber.count}x +
+ • Renders: {props.selectedFiber.count}x
- {tipisShown && !isMemoizable && ( +
-
-
-
+
+
+
How to stop renders
-
- Stop the following props, state and context from changing between - renders, and wrap the component in React.memo if not already +
+ Stop the following props, state and context from changing between renders, and wrap + the component in React.memo if not already
- )} + - {isMemoizable && ( +
-
-
-
+
+
+
No changes detected
-
+
This component would not have rendered if it was memoized
- )} -
+ +
Changed Props
- {selectedFiber.changes.props.length > 0 ? ( - selectedFiber.changes.props - .toSorted((a, b) => b.count - a.count) - .map((change) => ( + 0} + fallback={ +
+ No changes +
+ } + > + b.count - a.count)}> + {(change) => (
- {change.name} -
- {change.count}/{selectedFiber.count}x + {change.name} +
+ {change.count}/{props.selectedFiber.count}x
- )) - ) : ( -
- No changes -
- )} + )} + +
-
+
Changed State
- {selectedFiber.changes.state.length > 0 ? ( - selectedFiber.changes.state - .toSorted((a, b) => b.count - a.count) - .map((change) => ( + 0} + fallback={ +
+ No changes +
+ } + > + b.count - a.count)}> + {(change) => (
- - index {change.index} - -
- {change.count}/{selectedFiber.count}x + index {change.index} +
+ {change.count}/{props.selectedFiber.count}x
- )) - ) : ( -
- No changes -
- )} + )} + +
Changed Context
- {selectedFiber.changes.context.length > 0 ? ( - selectedFiber.changes.context - - .toSorted((a, b) => b.count - a.count) - .map((change) => ( + 0} + fallback={ +
+ No changes +
+ } + > + b.count - a.count)}> + {(change) => (
- {change.name} -
- {change.count}/{selectedFiber.count}x + {change.name} +
+ {change.count}/{props.selectedFiber.count}x
- )) - ) : ( -
- No changes -
- )} + )} + +
diff --git a/packages/scan/src/web/views/notifications/slowdown-history.tsx b/packages/scan/src/web/views/notifications/slowdown-history.tsx index d86f5968..526068f5 100644 --- a/packages/scan/src/web/views/notifications/slowdown-history.tsx +++ b/packages/scan/src/web/views/notifications/slowdown-history.tsx @@ -1,436 +1,347 @@ -import { useEffect, useRef, useState } from 'preact/compat'; -import { cn } from '~web/utils/helpers'; import { - InteractionEvent, + For, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, + type Accessor, + type Setter, +} from "solid-js"; +import { clearToolbarEvents } from "../../../core/notifications/event-tracking"; +import { iife } from "../../../core/notifications/performance-utils"; +import { cn } from "../../utils/helpers"; +import { + type CollapsedKeyboardInput, + CollapsedDroppedFrame, + CollapsedItem, +} from "./collapsed-event"; +import { NotificationEvent, getComponentName, getEventSeverity, getTotalTime, useNotificationsContext, -} from './data'; -import { - ClearIcon, - KeyboardIcon, - PointerIcon, - TrendingDownIcon, -} from './icons'; -import { Popover } from './popover'; -import { iife } from '~core/notifications/performance-utils'; -import { toolbarEventStore } from '~core/notifications/event-tracking'; -import { CollapsedDroppedFrame, CollapsedItem } from './collapsed-event'; +} from "./data"; +import { ClearIcon, KeyboardIcon, PointerIcon, TrendingDownIcon } from "./icons"; +import { Popover } from "./popover"; -const useFlashManager = (events: NotificationEvent[]) => { - const prevEventsRef = useRef([]); - const [newEventIds, setNewEventIds] = useState>(new Set()); - const isInitialMount = useRef(true); +const useFlashManager = (events: Accessor>) => { + let previousEvents: Array = []; + let isInitialRun = true; + const [newEventIds, setNewEventIds] = createSignal(new Set()); - useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - prevEventsRef.current = events; + createEffect(() => { + const currentEvents = events(); + if (isInitialRun) { + isInitialRun = false; + previousEvents = currentEvents; return; } - const currentIds = new Set(events.map((e) => e.id)); - const prevIds = new Set(prevEventsRef.current.map((e) => e.id)); + const previousIds = new Set(previousEvents.map((event) => event.id)); + const addedIds = new Set( + currentEvents.map((event) => event.id).filter((eventId) => !previousIds.has(eventId)), + ); - const newIds = new Set(); - currentIds.forEach((id) => { - if (!prevIds.has(id)) { - newIds.add(id); - } - }); - - if (newIds.size > 0) { - setNewEventIds(newIds); - setTimeout(() => { - setNewEventIds(new Set()); - }, 2000); + if (addedIds.size > 0) { + setNewEventIds(addedIds); + const timer = setTimeout(() => setNewEventIds(new Set()), 2000); + onCleanup(() => clearTimeout(timer)); } - prevEventsRef.current = events; - }, [events]); + previousEvents = currentEvents; + }); - return (id: string) => newEventIds.has(id); + return (eventId: string) => newEventIds().has(eventId); }; -const useFlash = ({ shouldFlash }: { shouldFlash: boolean }) => { - const [isFlashing, setIsFlashing] = useState(shouldFlash); - useEffect(() => { - if (shouldFlash) { - setIsFlashing(true); - const timer = setTimeout(() => { - setIsFlashing(false); - }, 1000); - return () => clearTimeout(timer); - } - }, [shouldFlash]); +const useFlash = (shouldFlash: Accessor) => { + const [isFlashing, setIsFlashing] = createSignal(shouldFlash()); + + createEffect(() => { + if (!shouldFlash()) return; + + setIsFlashing(true); + const timer = setTimeout(() => setIsFlashing(false), 1000); + onCleanup(() => clearTimeout(timer)); + }); return isFlashing; }; -export const SlowdownHistoryItem = ({ - event, - shouldFlash, -}: { - event: NotificationEvent; - shouldFlash: boolean; -}) => { +export const SlowdownHistoryItem = (props: { event: NotificationEvent; shouldFlash: boolean }) => { const { notificationState, setNotificationState } = useNotificationsContext(); + const isFlashing = useFlash(() => props.shouldFlash); - const severity = getEventSeverity(event); - - const isFlashing = useFlash({ shouldFlash }); - - switch (event.kind) { - case 'interaction': { - return ( -
-
-
- - ); - } - case 'dropped-frames': { - return ( - - ); - } - } -}; - -type CollapsedKeyboardInput = { - kind: 'collapsed-keyboard'; - events: Array; - timestamp: number; +
+ + {event.type === "click" ? ( + + ) : ( + + )} + + + {getComponentName(event.componentPath)} + +
+
+
+
+ {getTotalTime(event.timing).toFixed(0)}ms +
+
+
+ + ); + case "dropped-frames": + return ( + + ); + } + })()} + + ); }; type HistoryEvent = | { - kind: 'single'; + kind: "single"; event: NotificationEvent; timestamp: number; } | CollapsedKeyboardInput | CollapsedDroppedFrame; -const collapseEvents = (events: Array) => { - const newEvents = events.reduce>((prev, curr) => { - const lastEvent = prev.at(-1); +const collapseEvents = (events: Array) => + events.reduce>((collapsedEvents, currentEvent) => { + const lastEvent = collapsedEvents.at(-1); if (!lastEvent) { - return [ - { - kind: 'single', - event: curr, - timestamp: curr.timestamp, - }, - ]; + return [{ kind: "single", event: currentEvent, timestamp: currentEvent.timestamp }]; } switch (lastEvent.kind) { - case 'collapsed-keyboard': { + case "collapsed-keyboard": if ( - curr.kind === 'interaction' && - curr.type === 'keyboard' && - // must be on the same semantic component, it would be ideal to compare on fiberId, but i digress - curr.componentPath.join('-') === - lastEvent.events[0].componentPath.join('-') + currentEvent.kind === "interaction" && + currentEvent.type === "keyboard" && + currentEvent.componentPath.join("-") === lastEvent.events[0].componentPath.join("-") ) { - const eventsWithoutLast = prev.filter((e) => e !== lastEvent); - + const groupedEvents = [...lastEvent.events, currentEvent]; return [ - ...eventsWithoutLast, + ...collapsedEvents.filter((event) => event !== lastEvent), { - kind: 'collapsed-keyboard', - events: [...lastEvent.events, curr], - timestamp: Math.max( - ...[...lastEvent.events, curr].map((e) => e.timestamp), - ), + kind: "collapsed-keyboard", + events: groupedEvents, + timestamp: Math.max(...groupedEvents.map((event) => event.timestamp)), }, ]; } - - return [ - ...prev, - { - kind: 'single', - event: curr, - timestamp: curr.timestamp, - }, - ]; - } - case 'single': { - // if its a keyboard input on the same element + break; + case "single": if ( - lastEvent.event.kind === 'interaction' && - lastEvent.event.type === 'keyboard' && - curr.kind === 'interaction' && - curr.type === 'keyboard' && - lastEvent.event.componentPath.join('-') === - curr.componentPath.join('-') + lastEvent.event.kind === "interaction" && + lastEvent.event.type === "keyboard" && + currentEvent.kind === "interaction" && + currentEvent.type === "keyboard" && + lastEvent.event.componentPath.join("-") === currentEvent.componentPath.join("-") ) { - const eventsWithoutLast = prev.filter((e) => e !== lastEvent); return [ - ...eventsWithoutLast, + ...collapsedEvents.filter((event) => event !== lastEvent), { - kind: 'collapsed-keyboard', - events: [lastEvent.event, curr], - timestamp: Math.max(lastEvent.event.timestamp, curr.timestamp), + kind: "collapsed-keyboard", + events: [lastEvent.event, currentEvent], + timestamp: Math.max(lastEvent.event.timestamp, currentEvent.timestamp), }, ]; } - if ( - lastEvent.event.kind === 'dropped-frames' && - curr.kind === 'dropped-frames' - ) { - const eventsWithoutLast = prev.filter((e) => e !== lastEvent); - + if (lastEvent.event.kind === "dropped-frames" && currentEvent.kind === "dropped-frames") { return [ - ...eventsWithoutLast, + ...collapsedEvents.filter((event) => event !== lastEvent), { - kind: 'collapsed-frame-drops', - events: [lastEvent.event, curr], - timestamp: Math.max(lastEvent.event.timestamp, curr.timestamp), + kind: "collapsed-frame-drops", + events: [lastEvent.event, currentEvent], + timestamp: Math.max(lastEvent.event.timestamp, currentEvent.timestamp), }, ]; } - return [ - ...prev, - { - kind: 'single', - event: curr, - timestamp: curr.timestamp, - }, - ]; - } - case 'collapsed-frame-drops': { - if (curr.kind === 'dropped-frames') { - const eventsWithoutLast = prev.filter((e) => e !== lastEvent); + break; + case "collapsed-frame-drops": + if (currentEvent.kind === "dropped-frames") { + const groupedEvents = [...lastEvent.events, currentEvent]; return [ - ...eventsWithoutLast, + ...collapsedEvents.filter((event) => event !== lastEvent), { - kind: 'collapsed-frame-drops', - events: [...lastEvent.events, curr], - timestamp: Math.max( - ...[...lastEvent.events, curr].map((e) => e.timestamp), - ), + kind: "collapsed-frame-drops", + events: groupedEvents, + timestamp: Math.max(...groupedEvents.map((event) => event.timestamp)), }, ]; } - return [ - ...prev, - { - kind: 'single', - event: curr, - timestamp: curr.timestamp, - }, - ]; - } + break; } + + return [ + ...collapsedEvents, + { kind: "single", event: currentEvent, timestamp: currentEvent.timestamp }, + ]; }, []); - return newEvents; -}; -export const useLaggedEvents = (lagMs = 150) => { +export const useLaggedEvents = ( + lagMilliseconds = 150, +): [Accessor>, Setter>] => { const { notificationState } = useNotificationsContext(); - const [laggedEvents, setLaggedEvents] = useState(notificationState.events); + const [laggedEvents, setLaggedEvents] = createSignal>([ + ...notificationState.events, + ]); - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - setTimeout(() => { - setLaggedEvents(notificationState.events); - }, lagMs); - }, [notificationState.events]); - return [laggedEvents, setLaggedEvents] as const; + createEffect(() => { + const currentEvents = [...notificationState.events]; + const timer = setTimeout(() => setLaggedEvents(currentEvents), lagMilliseconds); + onCleanup(() => clearTimeout(timer)); + }); + + return [laggedEvents, setLaggedEvents]; }; export const SlowdownHistory = () => { const { notificationState, setNotificationState } = useNotificationsContext(); - const shouldFlash = useFlashManager(notificationState.events); + const shouldFlash = useFlashManager(() => notificationState.events); const [laggedEvents, setLaggedEvents] = useLaggedEvents(); - // this is to avoid a flicker from our overlapping events deduping logic. This should be handled downstream, but this simplifies logic for now - const collapsedEvents = collapseEvents(laggedEvents).toSorted( - (a, b) => b.timestamp - a.timestamp, + const collapsedEvents = createMemo(() => + collapseEvents(laggedEvents()).toSorted( + (firstEvent, secondEvent) => secondEvent.timestamp - firstEvent.timestamp, + ), ); return (
History { - toolbarEventStore.getState().actions.clear(); - setNotificationState((prev) => ({ - ...prev, + clearToolbarEvents(); + setNotificationState({ selectedEvent: null, selectedFiber: null, route: - prev.route === 'other-visualization' - ? 'other-visualization' - : 'render-visualization', - })); + notificationState.route === "other-visualization" + ? "other-visualization" + : "render-visualization", + }); setLaggedEvents([]); }} > - + } > -
- Clear all events -
+
Clear all events
-
- {collapsedEvents.length === 0 && ( -
- No Events -
- )} - {collapsedEvents.map((historyItem) => - iife(() => { - switch (historyItem.kind) { - case 'collapsed-keyboard': { - return ( - - ); - } - case 'single': { - return ( - - ); - } - case 'collapsed-frame-drops': { - return ( - - ); - } +
+ 0} + fallback={ +
+ No Events +
+ } + > + + {(historyItem) => + iife(() => { + switch (historyItem.kind) { + case "collapsed-keyboard": + case "collapsed-frame-drops": + return ; + case "single": + return ( + + ); + } + }) } - }), - )} + +
); diff --git a/packages/scan/src/web/views/toolbar/index.tsx b/packages/scan/src/web/views/toolbar/index.tsx index 27acd372..61bedf29 100644 --- a/packages/scan/src/web/views/toolbar/index.tsx +++ b/packages/scan/src/web/views/toolbar/index.tsx @@ -1,218 +1,234 @@ -import { useSignalEffect } from '@preact/signals'; -import { - useCallback, - useEffect, - useLayoutEffect, - useState, -} from 'preact/hooks'; -import { - type LocalStorageOptions, - ReactScanInternals, - Store, -} from '~core/index'; -import { Icon } from '~web/components/icon'; -import { Toggle } from '~web/components/toggle'; -import { signalWidgetViews } from '~web/state'; -import { cn, readLocalStorage, saveLocalStorage } from '~web/utils/helpers'; -import { constant } from '~web/utils/preact/constant'; -import { FPSMeter } from '~web/widget/fps-meter'; -import { getEventSeverity } from '../notifications/data'; -import { Notification } from '../notifications/icons'; -import { useAppNotifications } from '../notifications/notifications'; - -export const Toolbar = constant(() => { +import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js"; +import { type LocalStorageOptions, ReactScanInternals } from "../../../core/index"; +import { getInspectState, getOptionsState, setInspectState } from "../../../core/native-state"; +import { Icon } from "../../components/icon"; +import { Tooltip } from "../../components/tooltip"; +import { Toggle } from "../../components/toggle"; +import { NOTIFICATION_LAG_MS } from "../../constants"; +import { getWidgetView, setWidgetView } from "../../state"; +import { cn, readLocalStorage, saveLocalStorage } from "../../utils/helpers"; +import { FPSMeter } from "../../widget/fps-meter"; +import type { SnapEdge } from "../../widget/types"; +import { getEventSeverity } from "../notifications/data"; +import { Notification } from "../notifications/icons"; +import { useAppNotifications } from "../notifications/notifications"; + +interface ToolbarProps { + edge: SnapEdge; + onCollapse: (event: MouseEvent) => void; +} + +export const Toolbar = (props: ToolbarProps) => { const events = useAppNotifications(); - const [laggedEvents, setLaggedEvents] = useState(events); - - useEffect(() => { - const timeout = setTimeout(() => { - setLaggedEvents(events); - // 500 + buffer to never see intermediary state - // todo: check if we still need this large of buffer - }, 500 + 100); - return () => { - clearTimeout(timeout); - }; - }, [events]); - - const inspectState = Store.inspectState; - const isInspectActive = inspectState.value.kind === 'inspecting'; - const isInspectFocused = inspectState.value.kind === 'focused'; - - const [seenEvents, setSeenEvents] = useState>([]); + const [laggedEvents, setLaggedEvents] = createSignal(events()); + const [seenEvents, setSeenEvents] = createSignal>([]); + const [hoveredControl, setHoveredControl] = createSignal< + "inspect" | "notifications" | "collapse" | null + >(null); + const inspectState = getInspectState; + const isInspectActive = () => inspectState().kind === "inspecting"; + const isInspectFocused = () => inspectState().kind === "focused"; + + createEffect(() => { + const nextEvents = events(); + const timeoutId = setTimeout(() => { + setLaggedEvents(nextEvents); + }, NOTIFICATION_LAG_MS); + onCleanup(() => clearTimeout(timeoutId)); + }); - const onToggleInspect = useCallback(() => { - const currentState = Store.inspectState.value; + const onToggleInspect = () => { + const currentState = getInspectState(); switch (currentState.kind) { - case 'inspecting': { - signalWidgetViews.value = { - view: 'none', - }; - Store.inspectState.value = { - kind: 'inspect-off', - }; + case "inspecting": { + setWidgetView({ view: "none" }); + setInspectState({ kind: "inspect-off" }); return; } - - case 'focused': { - signalWidgetViews.value = { - view: 'inspector', - }; - Store.inspectState.value = { - kind: 'inspecting', + case "focused": { + setWidgetView({ view: "inspector" }); + setInspectState({ + kind: "inspecting", hoveredDomElement: null, - }; + }); return; } - // todo: auto select the root fibers first stateNode, and tell the user to select the element - case 'inspect-off': { - signalWidgetViews.value = { - view: 'none', - }; - Store.inspectState.value = { - kind: 'inspecting', + case "inspect-off": { + setWidgetView({ view: "none" }); + setInspectState({ + kind: "inspecting", hoveredDomElement: null, - }; + }); return; } - case 'uninitialized': { + case "uninitialized": { return; } } - }, []); - - const onToggleActive = useCallback((e: Event) => { - e.preventDefault(); - e.stopPropagation(); + }; + const onToggleActive = (event: Event) => { + event.preventDefault(); + event.stopPropagation(); if (!ReactScanInternals.instrumentation) { return; } - // todo: set a single source of truth - const isPaused = !ReactScanInternals.instrumentation.isPaused.value; - ReactScanInternals.instrumentation.isPaused.value = isPaused; - const existingLocalStorageOptions = - readLocalStorage('react-scan-options'); - saveLocalStorage('react-scan-options', { + const isPaused = !ReactScanInternals.instrumentation.getIsPaused(); + ReactScanInternals.instrumentation.setIsPaused(isPaused); + const existingLocalStorageOptions = readLocalStorage("react-scan-options"); + saveLocalStorage("react-scan-options", { ...existingLocalStorageOptions, enabled: !isPaused, }); - }, []); + }; - useSignalEffect(() => { - const state = Store.inspectState.value; - if (state.kind === 'uninitialized') { - Store.inspectState.value = { - kind: 'inspect-off', - }; + createEffect(() => { + const state = getInspectState(); + if (state.kind === "uninitialized") { + setInspectState({ kind: "inspect-off" }); } }); - let inspectIcon = null; - let inspectColor = '#999'; + createEffect(() => { + if (getWidgetView().view !== "notifications") return; + const ids = new Set(events().map((event) => event.id)); + setSeenEvents([...ids.values()]); + }); - if (isInspectActive) { - inspectIcon = ; - inspectColor = '#8e61e3'; - } else if (isInspectFocused) { - inspectIcon = ; - inspectColor = '#8e61e3'; - } else { - inspectIcon = ; - inspectColor = '#999'; - } + const inspectIconName = createMemo(() => (isInspectFocused() ? "icon-focus" : "icon-inspect")); + const isInspectSelected = () => isInspectActive() || isInspectFocused(); + const collapseRotation = () => { + switch (props.edge) { + case "top": + return "-rotate-90"; + case "bottom": + return "rotate-90"; + case "left": + return "rotate-180"; + case "right": + return ""; + } + }; + const tooltipPosition = (): "top" | "bottom" | "left" | "right" => { + switch (props.edge) { + case "top": + return "bottom"; + case "bottom": + return "top"; + case "left": + return "right"; + case "right": + return "left"; + } + }; - // oxlint-disable-next-line react-hooks/exhaustive-deps - useLayoutEffect(() => { - if (signalWidgetViews.value.view !== 'notifications') { + const showNotifications = () => { + if (getInspectState().kind !== "inspect-off") { + setInspectState({ kind: "inspect-off" }); + } + if (getWidgetView().view === "notifications") { + setWidgetView({ view: "none" }); return; } - const ids = new Set(events.map((event) => event.id)); - setSeenEvents([...ids.values()]); - }, [events.length, signalWidgetViews.value.view]); + setSeenEvents(events().map((event) => event.id)); + setWidgetView({ view: "notifications" }); + }; return ( -
-
+
+
+ +
+
setHoveredControl("inspect")} + onMouseLeave={() => setHoveredControl(null)} + > + + Inspect element +
- -
+
setHoveredControl("notifications")} + onMouseLeave={() => setHoveredControl(null)} + > + + Notifications +
- - - {/* todo add back showFPS*/} - {ReactScanInternals.options.value.showFPS && } + + + +
setHoveredControl("collapse")} + onMouseLeave={() => setHoveredControl(null)} + > + + + Collapse + +
); -}); +}; diff --git a/packages/scan/src/web/widget/collapsed-indicator.tsx b/packages/scan/src/web/widget/collapsed-indicator.tsx new file mode 100644 index 00000000..22e3ede1 --- /dev/null +++ b/packages/scan/src/web/widget/collapsed-indicator.tsx @@ -0,0 +1,28 @@ +import type { SnapEdge } from "./types"; +import { Icon } from "../components/icon"; + +interface CollapsedIndicatorProps { + edge: SnapEdge; + onExpand: (event: MouseEvent) => void; +} + +export const CollapsedIndicator = (props: CollapsedIndicatorProps) => ( + +); diff --git a/packages/scan/src/web/widget/fps-meter.tsx b/packages/scan/src/web/widget/fps-meter.tsx index 898ffd4a..0028be17 100644 --- a/packages/scan/src/web/widget/fps-meter.tsx +++ b/packages/scan/src/web/widget/fps-meter.tsx @@ -1,8 +1,13 @@ -import { useEffect, useState } from "preact/hooks"; -import { getFPS } from "~core/instrumentation"; -import { cn } from "~web/utils/helpers"; +import { createSignal, onCleanup, onMount, Show } from "solid-js"; +import { getFPS } from "../../core/instrumentation"; +import { FPS_SAMPLE_INTERVAL_MS } from "../constants"; +import { cn } from "../utils/helpers"; -const FpsMeterInner = ({ fps }: { fps: number }) => { +interface FpsMeterInnerProps { + fps: number; +} + +const FpsMeterInner = (props: FpsMeterInnerProps) => { const getColor = (fps: number) => { if (fps < 30) return "#EF4444"; if (fps < 50) return "#F59E0B"; @@ -11,7 +16,7 @@ const FpsMeterInner = ({ fps }: { fps: number }) => { return (
{ )} >
- {fps} + {props.fps}
- - FPS - + FPS
); }; export const FPSMeter = () => { - const [fps, setFps] = useState(null); + const [fps, setFps] = createSignal(); - useEffect(() => { + onMount(() => { const intervalId = setInterval(() => { setFps(getFPS()); - }, 200); - - return () => clearInterval(intervalId); - }, []); + }, FPS_SAMPLE_INTERVAL_MS); + onCleanup(() => clearInterval(intervalId)); + }); return (
- {/* fixme: default fps state*/} - {fps === null ? <>️ : } + {(currentFps) => }
); }; diff --git a/packages/scan/src/web/widget/header.tsx b/packages/scan/src/web/widget/header.tsx index 335fe5bd..6ace9053 100644 --- a/packages/scan/src/web/widget/header.tsx +++ b/packages/scan/src/web/widget/header.tsx @@ -1,49 +1,50 @@ -import { useSignal } from "@preact/signals"; -import { useEffect, useRef } from "preact/hooks"; -import { Store } from "~core/index"; -import { Icon } from "~web/components/icon"; -import { COPY_FEEDBACK_DURATION_MS } from "~web/constants"; -import { useDelayedValue } from "~web/hooks/use-delayed-value"; -import { signalWidgetViews } from "~web/state"; -import { copyFocusedElement } from "~web/utils/copy-focused-element"; -import { hasNonEmptyTextSelection } from "~web/utils/has-non-empty-text-selection"; -import { cn } from "~web/utils/helpers"; -import { isInputLikeFocused } from "~web/utils/is-input-like-focused"; -import { isMac } from "~web/utils/is-mac"; -import { isUserReactGrabActive } from "~web/utils/is-user-react-grab-active"; -import { HeaderInspect } from "~web/views/inspector/header"; +import { createSignal, onCleanup, onMount, Show } from "solid-js"; +import { getInspectState, setInspectState } from "../../core/native-state"; +import { Icon } from "../components/icon"; +import { COPY_FEEDBACK_DURATION_MS, HEADER_TRANSITION_DELAY_MS } from "../constants"; +import { createDelayedValue } from "../hooks/use-delayed-value"; +import { getWidgetView, setWidgetView } from "../state"; +import { copyFocusedElement } from "../utils/copy-focused-element"; +import { hasNonEmptyTextSelection } from "../utils/has-non-empty-text-selection"; +import { cn } from "../utils/helpers"; +import { isInputLikeFocused } from "../utils/is-input-like-focused"; +import { isMac } from "../utils/is-mac"; +import { isUserReactGrabActive } from "../utils/is-user-react-grab-active"; +import { HeaderInspect } from "../views/inspector/header"; export const Header = () => { - const isInitialView = useDelayedValue(Store.inspectState.value.kind === "focused", 150, 0); - const isCopied = useSignal(false); + const isInitialView = createDelayedValue( + () => getInspectState().kind === "focused", + HEADER_TRANSITION_DELAY_MS, + 0, + ); + const [isCopied, setIsCopied] = createSignal(false); + let copyTimeoutId: ReturnType | undefined; const handleClose = () => { - signalWidgetViews.value = { + setWidgetView({ view: "none", - }; - Store.inspectState.value = { + }); + setInspectState({ kind: "inspect-off", - }; + }); }; const handleCopy = async () => { - const state = Store.inspectState.value; + const state = getInspectState(); if (state.kind !== "focused" || !state.focusedDomElement) return; const didCopy = await copyFocusedElement(state.focusedDomElement); if (!didCopy) return; - isCopied.value = true; - setTimeout(() => { - isCopied.value = false; + setIsCopied(true); + copyTimeoutId = setTimeout(() => { + setIsCopied(false); handleClose(); }, COPY_FEEDBACK_DURATION_MS); }; - const refHandleCopy = useRef(handleCopy); - refHandleCopy.current = handleCopy; - - useEffect(() => { + onMount(() => { const onKeyDown = (event: KeyboardEvent) => { - const state = Store.inspectState.value; + const state = getInspectState(); if (state.kind !== "focused" || !state.focusedDomElement) return; if (isUserReactGrabActive()) return; if (!(event.metaKey || event.ctrlKey)) return; @@ -53,49 +54,46 @@ export const Header = () => { event.preventDefault(); event.stopImmediatePropagation(); - void refHandleCopy.current(); + void handleCopy(); }; document.addEventListener("keydown", onKeyDown, { capture: true }); - return () => { + onCleanup(() => { document.removeEventListener("keydown", onKeyDown, { capture: true }); - }; - }, []); - - const isHeaderIsNotifications = signalWidgetViews.value.view === "notifications"; + clearTimeout(copyTimeoutId); + }); + }); - if (isHeaderIsNotifications) { - return; - } - - const isFocused = Store.inspectState.value.kind === "focused"; + const isFocused = () => getInspectState().kind === "focused"; const copyShortcutLabel = isMac() ? "⌘C" : "Ctrl+C"; return ( -
-
-
- + +
+
+
+ +
-
- {isFocused && ( - - )} + + + - -
+ +
+ ); }; diff --git a/packages/scan/src/web/widget/helpers.ts b/packages/scan/src/web/widget/helpers.ts index f0984adb..c08f190a 100644 --- a/packages/scan/src/web/widget/helpers.ts +++ b/packages/scan/src/web/widget/helpers.ts @@ -1,6 +1,6 @@ -import { MIN_SIZE } from '../constants'; -import { getSafeArea, type SafeAreaInsets } from '../utils/safe-area'; -import type { Corner, Position, ResizeHandleProps, Size } from './types'; +import { MIN_SIZE } from "../constants"; +import { getSafeArea, type ResolvedSafeAreaInsets } from "../utils/safe-area"; +import type { Corner, Position, ResizeHandleProps, Size } from "./types"; class WindowDimensions { maxWidth: number; @@ -9,7 +9,7 @@ class WindowDimensions { constructor( public width: number, public height: number, - public safeArea: SafeAreaInsets, + public safeArea: ResolvedSafeAreaInsets, ) { this.maxWidth = width - safeArea.left - safeArea.right; this.maxHeight = height - safeArea.top - safeArea.bottom; @@ -34,11 +34,8 @@ class WindowDimensions { let cachedWindowDimensions: WindowDimensions | undefined; -const safeAreaMatches = (a: SafeAreaInsets, b: SafeAreaInsets): boolean => - a.top === b.top && - a.right === b.right && - a.bottom === b.bottom && - a.left === b.left; +const safeAreaMatches = (a: ResolvedSafeAreaInsets, b: ResolvedSafeAreaInsets): boolean => + a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left; export const getWindowDimensions = () => { const currentWidth = window.innerWidth; @@ -54,17 +51,13 @@ export const getWindowDimensions = () => { return cachedWindowDimensions; } - cachedWindowDimensions = new WindowDimensions( - currentWidth, - currentHeight, - currentSafeArea, - ); + cachedWindowDimensions = new WindowDimensions(currentWidth, currentHeight, currentSafeArea); return cachedWindowDimensions; }; export const getOppositeCorner = ( - position: ResizeHandleProps['position'], + position: ResizeHandleProps["position"], currentCorner: Corner, isFullScreen: boolean, isFullWidth?: boolean, @@ -72,43 +65,35 @@ export const getOppositeCorner = ( ): Corner => { // For full screen mode if (isFullScreen) { - if (position === 'top-left') return 'bottom-right'; - if (position === 'top-right') return 'bottom-left'; - if (position === 'bottom-left') return 'top-right'; - if (position === 'bottom-right') return 'top-left'; - - const [vertical, horizontal] = currentCorner.split('-'); - if (position === 'left') return `${vertical}-right` as Corner; - if (position === 'right') return `${vertical}-left` as Corner; - if (position === 'top') return `bottom-${horizontal}` as Corner; - if (position === 'bottom') return `top-${horizontal}` as Corner; + if (position === "top-left") return "bottom-right"; + if (position === "top-right") return "bottom-left"; + if (position === "bottom-left") return "top-right"; + if (position === "bottom-right") return "top-left"; + + const [vertical, horizontal] = currentCorner.split("-"); + if (position === "left") return `${vertical}-right` as Corner; + if (position === "right") return `${vertical}-left` as Corner; + if (position === "top") return `bottom-${horizontal}` as Corner; + if (position === "bottom") return `top-${horizontal}` as Corner; } // 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 => { - const isRTL = getComputedStyle(document.body).direction === 'rtl'; +export const calculatePosition = (corner: Corner, width: number, height: number): Position => { + const isRTL = getComputedStyle(document.body).direction === "rtl"; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; @@ -130,7 +115,7 @@ export const calculatePosition = ( let y: number; let leftBound = safeArea.left; - let rightBound = windowWidth - effectiveWidth - safeArea.right; + let rightBound = windowWidth - effectiveWidth - safeArea.right; let topBound = safeArea.top; let bottomBound = windowHeight - effectiveHeight - safeArea.bottom; @@ -146,19 +131,19 @@ export const calculatePosition = ( const rtlLeftCornerX = -(windowWidth - effectiveWidth - safeArea.left); switch (corner) { - case 'top-right': + case "top-right": x = isRTL ? rtlRightCornerX : rightBound; y = topBound; break; - case 'bottom-right': + case "bottom-right": x = isRTL ? rtlRightCornerX : rightBound; y = bottomBound; break; - case 'bottom-left': + case "bottom-left": x = isRTL ? rtlLeftCornerX : leftBound; y = bottomBound; break; - case 'top-left': + case "top-left": x = isRTL ? rtlLeftCornerX : leftBound; y = topBound; break; @@ -171,35 +156,26 @@ export const calculatePosition = ( // Only ensure positions are within bounds if minimized if (isMinimized) { if (isRTL) { - x = Math.min( - rtlRightCornerX, - Math.max(x, rtlLeftCornerX), - ); + x = Math.min(rtlRightCornerX, Math.max(x, rtlLeftCornerX)); } else { - x = Math.max( - leftBound, - Math.min(x, rightBound), - ); + x = Math.max(leftBound, Math.min(x, rightBound)); } - y = Math.max( - topBound, - Math.min(y, bottomBound), - ); + y = Math.max(topBound, Math.min(y, bottomBound)); } return { x, y }; }; const positionMatchesCorner = ( - position: ResizeHandleProps['position'], + position: ResizeHandleProps["position"], corner: Corner, ): boolean => { - const [vertical, horizontal] = corner.split('-'); + const [vertical, horizontal] = corner.split("-"); return position !== vertical && position !== horizontal; }; export const getHandleVisibility = ( - position: ResizeHandleProps['position'], + position: ResizeHandleProps["position"], corner: Corner, isFullWidth: boolean, isFullHeight: boolean, @@ -215,39 +191,25 @@ export const getHandleVisibility = ( // Full width state if (isFullWidth) { - return position !== corner.split('-')[0]; + return position !== corner.split("-")[0]; } // Full height state if (isFullHeight) { - return position !== corner.split('-')[1]; + return position !== corner.split("-")[1]; } return false; }; -export const calculateBoundedSize = ( - currentSize: number, - delta: number, - isWidth: boolean, -): number => { - const min = isWidth ? MIN_SIZE.width : MIN_SIZE.initialHeight; - const max = isWidth - ? getWindowDimensions().maxWidth - : getWindowDimensions().maxHeight; - - const newSize = currentSize + delta; - return Math.min(Math.max(min, newSize), max); -}; - export const calculateNewSizeAndPosition = ( - position: ResizeHandleProps['position'], + position: ResizeHandleProps["position"], initialSize: Size, initialPosition: Position, deltaX: number, deltaY: number, ): { newSize: Size; newPosition: Position } => { - const isRTL = getComputedStyle(document.body).direction === 'rtl'; + const isRTL = getComputedStyle(document.body).direction === "rtl"; const safeArea = getSafeArea(); const maxWidth = window.innerWidth - safeArea.left - safeArea.right; @@ -259,27 +221,27 @@ export const calculateNewSizeAndPosition = ( let newY = initialPosition.y; // horizontal resize for RTL - if (isRTL && position.includes('right')) { + if (isRTL && position.includes("right")) { // Check if we have enough space on the right const availableWidth = -initialPosition.x + initialSize.width - safeArea.right; const proposedWidth = Math.min(initialSize.width + deltaX, availableWidth); newWidth = Math.min(maxWidth, Math.max(MIN_SIZE.width, proposedWidth)); newX = initialPosition.x + (newWidth - initialSize.width); } - if (isRTL && position.includes('left')) { + if (isRTL && position.includes("left")) { // Check if we have enough space on the left const availableWidth = window.innerWidth - initialPosition.x - safeArea.left; const proposedWidth = Math.min(initialSize.width - deltaX, availableWidth); newWidth = Math.min(maxWidth, Math.max(MIN_SIZE.width, proposedWidth)); } // horizontal resize for LTR - if (!isRTL && position.includes('right')) { + if (!isRTL && position.includes("right")) { // Check if we have enough space on the right const availableWidth = window.innerWidth - initialPosition.x - safeArea.right; const proposedWidth = Math.min(initialSize.width + deltaX, availableWidth); newWidth = Math.min(maxWidth, Math.max(MIN_SIZE.width, proposedWidth)); } - if (!isRTL && position.includes('left')) { + if (!isRTL && position.includes("left")) { // Check if we have enough space on the left const availableWidth = initialPosition.x + initialSize.width - safeArea.left; const proposedWidth = Math.min(initialSize.width - deltaX, availableWidth); @@ -288,29 +250,17 @@ export const calculateNewSizeAndPosition = ( } // vertical resize - if (position.includes('bottom')) { + if (position.includes("bottom")) { // Check if we have enough space at the bottom const availableHeight = window.innerHeight - initialPosition.y - safeArea.bottom; - const proposedHeight = Math.min( - initialSize.height + deltaY, - availableHeight, - ); - newHeight = Math.min( - maxHeight, - Math.max(MIN_SIZE.initialHeight, proposedHeight), - ); + const proposedHeight = Math.min(initialSize.height + deltaY, availableHeight); + newHeight = Math.min(maxHeight, Math.max(MIN_SIZE.initialHeight, proposedHeight)); } - if (position.includes('top')) { + if (position.includes("top")) { // Check if we have enough space at the top const availableHeight = initialPosition.y + initialSize.height - safeArea.top; - const proposedHeight = Math.min( - initialSize.height - deltaY, - availableHeight, - ); - newHeight = Math.min( - maxHeight, - Math.max(MIN_SIZE.initialHeight, proposedHeight), - ); + const proposedHeight = Math.min(initialSize.height - deltaY, availableHeight); + newHeight = Math.min(maxHeight, Math.max(MIN_SIZE.initialHeight, proposedHeight)); newY = initialPosition.y - (newHeight - initialSize.height); } @@ -327,21 +277,12 @@ export const calculateNewSizeAndPosition = ( // Ensure position stays within bounds if (isRTL) { - newX = Math.min( - rtlRightCornerX, - Math.max(newX, rtlLeftCornerX), - ); + newX = Math.min(rtlRightCornerX, Math.max(newX, rtlLeftCornerX)); } else { - newX = Math.max( - leftBound, - Math.min(newX, rightBound), - ); + newX = Math.max(leftBound, Math.min(newX, rightBound)); } - newY = Math.max( - topBound, - Math.min(newY, bottomBound), - ); + newY = Math.max(topBound, Math.min(newY, bottomBound)); return { newSize: { width: newWidth, height: newHeight }, @@ -353,16 +294,13 @@ export const getClosestCorner = (position: Position): Corner => { const windowDims = getWindowDimensions(); const distances: Record = { - 'top-left': Math.hypot(position.x, 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, - ), + "top-left": Math.hypot(position.x, 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), }; - let closest: Corner = 'top-left'; + let closest: Corner = "top-left"; for (const key in distances) { if (distances[key as Corner] < distances[closest]) { @@ -372,57 +310,3 @@ export const getClosestCorner = (position: Position): Corner => { return closest; }; - -// 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/web/widget/index.tsx b/packages/scan/src/web/widget/index.tsx index 7025eb03..acfd4482 100644 --- a/packages/scan/src/web/widget/index.tsx +++ b/packages/scan/src/web/widget/index.tsx @@ -1,760 +1,288 @@ -import { createContext, type JSX } from "preact"; -import { useCallback, useEffect, useRef, useState } from "preact/hooks"; -import { Store, ReactScanInternals } from "~core/index"; import { - cn, - saveLocalStorage, - removeLocalStorage, - readLocalStorage, -} from "~web/utils/helpers"; -import { Content } from "~web/views"; -import { ScanOverlay } from "~web/views/inspector/overlay"; + createContext, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, + Show, +} from "solid-js"; +import { getInspectState } from "../../core/native-state"; import { + TOOLBAR_DEFAULT_HEIGHT_PX, + TOOLBAR_DEFAULT_WIDTH_PX, + TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS, + TOOLBAR_FADE_IN_DELAY_MS, LOCALSTORAGE_KEY, - LOCALSTORAGE_COLLAPSED_KEY, - MIN_SIZE, - LOCALSTORAGE_LAST_VIEW_KEY, - TOOLBAR_INTERACTIVE_SELECTOR, + TOOLBAR_SNAP_ANIMATION_DURATION_MS, + TOOLBAR_Z_INDEX, } from "../constants"; import { - getDefaultWidgetConfig, - signalRefWidget, - signalWidget, - signalWidgetViews, - updateDimensions, - type WidgetStates, + getToolbarState, + getWidgetState, + getWidgetView, + setWidgetRef, + setWidgetState, + setWidgetView, + updateToolbarState, } from "../state"; +import { createToolbarDrag } from "../utils/create-toolbar-drag"; +import { getCornerFromEdge } from "../utils/get-corner-from-edge"; +import { saveLocalStorage } from "../utils/helpers"; import { getSafeArea } from "../utils/safe-area"; import { - calculateBoundedSize, - calculatePosition, - getBestCorner, -} from "./helpers"; + getCollapsedDimensions, + getCollapsedPosition, + getPositionFromEdgeAndRatio, +} from "../utils/toolbar-position"; +import { Content } from "../views"; +import { Toolbar } from "../views/toolbar"; +import { ScanOverlay } from "../views/inspector/overlay"; +import { calculatePosition } from "./helpers"; +import { CollapsedIndicator } from "./collapsed-indicator"; import { ResizeHandle } from "./resize-handle"; -import { signalWidgetCollapsed } from "~web/state"; -import { Icon } from "~web/components/icon"; -import { Corner } from "./types"; -import type { CollapsedPosition } from "./types"; +import type { Position, SnapEdge } from "./types"; -const COLLAPSED_SIZE = { - horizontal: { width: 20, height: 48 }, - vertical: { width: 48, height: 20 }, -} as const; +export const ToolbarElementContext = createContext(null); export const Widget = () => { - const refWidget = useRef(null); - const refShouldOpen = useRef(false); - - const refInitialMinimizedWidth = useRef(0); - const refInitialMinimizedHeight = useRef(0); - const refExpandingFromCollapsed = useRef(false); - - const updateWidgetPosition = useCallback((shouldSave = true) => { - if (!refWidget.current) return; - - const { corner } = signalWidget.value; - let newWidth: number; - let newHeight: number; - - if (signalWidgetCollapsed.value) { - const orientation = - signalWidgetCollapsed.value.orientation || "horizontal"; - const size = COLLAPSED_SIZE[orientation]; - newWidth = size.width; - newHeight = size.height; - } else if (refShouldOpen.current) { - const lastDims = signalWidget.value.lastDimensions; - newWidth = calculateBoundedSize(lastDims.width, 0, true); - newHeight = calculateBoundedSize(lastDims.height, 0, false); - - if (refExpandingFromCollapsed.current) { - refExpandingFromCollapsed.current = false; - } - } else { - newWidth = refInitialMinimizedWidth.current; - newHeight = refInitialMinimizedHeight.current; - } - - const newPosition = calculatePosition(corner, newWidth, newHeight); - - // When collapsed, override position so arrow is flush against the viewport edge. - let finalPosition = newPosition; - if (signalWidgetCollapsed.value) { - const { corner: collapsedCorner, orientation = "horizontal" } = - signalWidgetCollapsed.value; - const size = COLLAPSED_SIZE[orientation]; - const safeArea = getSafeArea(); - - switch (collapsedCorner) { - case "top-left": - finalPosition = - orientation === "horizontal" - ? { x: -1, y: safeArea.top } - : { x: safeArea.left, y: -1 }; - break; - case "bottom-left": - finalPosition = - orientation === "horizontal" - ? { x: -1, y: window.innerHeight - size.height - safeArea.bottom } - : { x: safeArea.left, y: window.innerHeight - size.height + 1 }; - break; - case "top-right": - finalPosition = - orientation === "horizontal" - ? { x: window.innerWidth - size.width + 1, y: safeArea.top } - : { x: window.innerWidth - size.width - safeArea.right, y: -1 }; - break; - case "bottom-right": - default: - finalPosition = - orientation === "horizontal" - ? { - x: window.innerWidth - size.width + 1, - y: window.innerHeight - size.height - safeArea.bottom, - } - : { - x: window.innerWidth - size.width - safeArea.right, - y: window.innerHeight - size.height + 1, - }; - break; - } - } - - const isTooSmall = - newWidth < MIN_SIZE.width || newHeight < MIN_SIZE.initialHeight; - const shouldPersist = shouldSave && !isTooSmall; - - const container = refWidget.current; - const containerStyle = container.style; - - let rafId: number | null = null; - const onTransitionEnd = () => { - updateDimensions(); - container.removeEventListener("transitionend", onTransitionEnd); - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - }; - - container.addEventListener("transitionend", onTransitionEnd); - containerStyle.transition = "all 0.25s cubic-bezier(0, 0, 0.2, 1)"; + let toolbarContainer: HTMLDivElement | undefined; + let panelContainer: HTMLDivElement | undefined; + let fadeTimeoutId: ReturnType | undefined; + let resizeFrameId: number | undefined; + const [isLoaded, setIsLoaded] = createSignal(false); + const [isToolbarHovered, setIsToolbarHovered] = createSignal(false); + const [expandedDimensions, setExpandedDimensions] = createSignal({ + width: TOOLBAR_DEFAULT_WIDTH_PX, + height: TOOLBAR_DEFAULT_HEIGHT_PX, + }); + const [toolbarPosition, setToolbarPosition] = createSignal( + getPositionFromEdgeAndRatio( + getToolbarState().edge, + getToolbarState().ratio, + TOOLBAR_DEFAULT_WIDTH_PX, + TOOLBAR_DEFAULT_HEIGHT_PX, + ), + ); - rafId = requestAnimationFrame(() => { - containerStyle.width = `${newWidth}px`; - containerStyle.height = `${newHeight}px`; - containerStyle.transform = `translate3d(${finalPosition.x}px, ${finalPosition.y}px, 0)`; - rafId = null; + const isCollapsed = () => getToolbarState().collapsed; + const isPanelOpen = createMemo( + () => getWidgetView().view !== "none" || getInspectState().kind === "focused", + ); + const collapsedDimensions = () => getCollapsedDimensions(getToolbarState().edge); + const visibleToolbarPosition = () => + isCollapsed() + ? getCollapsedPosition( + getToolbarState().edge, + toolbarPosition(), + expandedDimensions(), + collapsedDimensions(), + ) + : toolbarPosition(); + + const savePanelState = () => { + const widgetState = getWidgetState(); + saveLocalStorage(LOCALSTORAGE_KEY, { + corner: widgetState.corner, + dimensions: widgetState.dimensions, + lastDimensions: widgetState.lastDimensions, + componentsTree: widgetState.componentsTree, }); - - const safeArea = getSafeArea(); - const newDimensions = { - isFullWidth: newWidth >= window.innerWidth - safeArea.left - safeArea.right, - isFullHeight: newHeight >= window.innerHeight - safeArea.top - safeArea.bottom, - width: newWidth, - height: newHeight, - position: finalPosition, - }; - - signalWidget.value = { + }; + + const updatePanelCorner = (edge: SnapEdge, ratio: number) => { + const corner = getCornerFromEdge(edge, ratio); + const dimensions = getWidgetState().dimensions; + const position = calculatePosition(corner, dimensions.width, dimensions.height); + setWidgetState((state) => ({ + ...state, corner, - dimensions: newDimensions, - lastDimensions: refShouldOpen - ? signalWidget.value.lastDimensions - : newWidth > refInitialMinimizedWidth.current - ? newDimensions - : signalWidget.value.lastDimensions, - componentsTree: signalWidget.value.componentsTree, - }; - - if (shouldPersist) { - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: signalWidget.value.corner, - dimensions: signalWidget.value.dimensions, - lastDimensions: signalWidget.value.lastDimensions, - componentsTree: signalWidget.value.componentsTree, - }); - } - - updateDimensions(); - }, []); - - const handleDrag = useCallback( - (e: JSX.TargetedPointerEvent) => { - const target = e.target as HTMLElement; - - // Skip drag on interactive/text-selectable surfaces so users can select - // prompt text, focus inputs, and click buttons normally. - if (target.closest(TOOLBAR_INTERACTIVE_SELECTOR)) { - return; - } - - e.preventDefault(); - - if (!refWidget.current) return; - - const container = refWidget.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; - - let currentX = initialX; - let currentY = initialY; - let rafId: number | null = null; - let hasMoved = false; - let lastMouseX = initialMouseX; - let lastMouseY = initialMouseY; - - const handlePointerMove = (e: globalThis.PointerEvent) => { - if (rafId) return; - - hasMoved = true; - lastMouseX = e.clientX; - lastMouseY = e.clientY; - - rafId = requestAnimationFrame(() => { - const deltaX = lastMouseX - initialMouseX; - const deltaY = lastMouseY - initialMouseY; - - currentX = Number(initialX) + deltaX; - currentY = Number(initialY) + deltaY; - - containerStyle.transition = "none"; - containerStyle.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; - - const widgetRight = currentX + dimensions.width; - const widgetBottom = currentY + dimensions.height; - - const outsideLeft = Math.max(0, -currentX); - const outsideRight = Math.max(0, widgetRight - window.innerWidth); - const outsideTop = Math.max(0, -currentY); - const outsideBottom = Math.max(0, widgetBottom - window.innerHeight); - - const horizontalOutside = Math.min( - dimensions.width, - outsideLeft + outsideRight - ); - const verticalOutside = Math.min( - dimensions.height, - outsideTop + outsideBottom - ); - const areaOutside = - horizontalOutside * dimensions.height + - verticalOutside * dimensions.width - - horizontalOutside * verticalOutside; - const totalArea = dimensions.width * dimensions.height; - - // todo: delete this doesn't do anything - let shouldCollapse = areaOutside > totalArea * 0.35; - - if (!shouldCollapse && ReactScanInternals.options.value.showFPS) { - const fpsRight = currentX + dimensions.width; - const fpsLeft = fpsRight - 100; - - const fpsFullyOutside = - fpsRight <= 0 || - fpsLeft >= window.innerWidth || - currentY + dimensions.height <= 0 || - currentY >= window.innerHeight; - - shouldCollapse = fpsFullyOutside; - } - - if (shouldCollapse) { - const widgetCenterX = currentX + dimensions.width / 2; - const widgetCenterY = currentY + dimensions.height / 2; - const screenCenterX = window.innerWidth / 2; - const screenCenterY = window.innerHeight / 2; - - let targetCorner: Corner; - if (widgetCenterX < screenCenterX) { - targetCorner = - widgetCenterY < screenCenterY ? "top-left" : "bottom-left"; - } else { - targetCorner = - widgetCenterY < screenCenterY ? "top-right" : "bottom-right"; - } - - let orientation: "horizontal" | "vertical"; - const horizontalOverflow = Math.max(outsideLeft, outsideRight); - const verticalOverflow = Math.max(outsideTop, outsideBottom); - - orientation = - horizontalOverflow > verticalOverflow ? "horizontal" : "vertical"; - - signalWidget.value = { - ...signalWidget.value, - corner: targetCorner, - lastDimensions: { - ...dimensions, - position: calculatePosition( - targetCorner, - dimensions.width, - dimensions.height - ), - }, - }; - - const collapsedPosition: CollapsedPosition = { - corner: targetCorner, - orientation, - }; - - signalWidgetCollapsed.value = collapsedPosition; - saveLocalStorage(LOCALSTORAGE_COLLAPSED_KEY, collapsedPosition); - saveLocalStorage(LOCALSTORAGE_KEY, signalWidget.value); - updateWidgetPosition(false); - - document.removeEventListener("pointermove", handlePointerMove); - document.removeEventListener("pointerup", handlePointerEnd); - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - } - - rafId = null; - }); - }; - - const handlePointerEnd = () => { - if (!container) return; - - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - - document.removeEventListener("pointermove", handlePointerMove); - document.removeEventListener("pointerup", handlePointerEnd); - - // Calculate total movement distance - const totalDeltaX = Math.abs(lastMouseX - initialMouseX); - const totalDeltaY = Math.abs(lastMouseY - initialMouseY); - const totalMovement = Math.sqrt( - totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY - ); - - // Only consider it a move if we moved more than 60 pixels - if (!hasMoved || totalMovement < 60) return; - - const newCorner = getBestCorner( - lastMouseX, - lastMouseY, - initialMouseX, - initialMouseY, - Store.inspectState.value.kind === "focused" ? 80 : 40 - ); - - if (newCorner === signalWidget.value.corner) { - containerStyle.transition = - "transform 0.25s cubic-bezier(0, 0, 0.2, 1)"; - const currentPosition = signalWidget.value.dimensions.position; - requestAnimationFrame(() => { - containerStyle.transform = `translate3d(${currentPosition.x}px, ${currentPosition.y}px, 0)`; - }); - - return; - } - - 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; - } - }; - - container.addEventListener("transitionend", onTransitionEnd); - containerStyle.transition = - "transform 0.25s cubic-bezier(0, 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, - componentsTree: signalWidget.value.componentsTree, - }; - - saveLocalStorage(LOCALSTORAGE_KEY, { - corner: newCorner, - dimensions: signalWidget.value.dimensions, - lastDimensions: signalWidget.value.lastDimensions, - componentsTree: signalWidget.value.componentsTree, - }); - }; - - document.addEventListener("pointermove", handlePointerMove); - document.addEventListener("pointerup", handlePointerEnd); + dimensions: { + ...dimensions, + position, + }, + })); + savePanelState(); + }; + + const updateExpandedPosition = () => { + const dimensions = expandedDimensions(); + setToolbarPosition( + getPositionFromEdgeAndRatio( + getToolbarState().edge, + getToolbarState().ratio, + dimensions.width, + dimensions.height, + ), + ); + }; + + const measureExpandedToolbar = () => { + if (!toolbarContainer || isCollapsed()) return; + const bounds = toolbarContainer.getBoundingClientRect(); + if (bounds.width <= 0 || bounds.height <= 0) return; + setExpandedDimensions({ + width: bounds.width, + height: bounds.height, + }); + updateExpandedPosition(); + }; + + const toolbarDrag = createToolbarDrag({ + getContainer: () => toolbarContainer, + isCollapsed, + onDragStart: () => { + setWidgetView({ view: "none" }); }, - [] - ); - - const handleCollapsedDrag = useCallback( - (e: JSX.TargetedPointerEvent) => { - e.preventDefault(); - - if (!refWidget.current || !signalWidgetCollapsed.value) return; - - const { corner: collapsedCorner, orientation = "horizontal" } = - signalWidgetCollapsed.value; - - const initialMouseX = e.clientX; - const initialMouseY = e.clientY; - - let rafId: number | null = null; - let hasExpanded = false; - - const DRAG_THRESHOLD = 50; - - const handlePointerMove = (e: globalThis.PointerEvent) => { - if (hasExpanded || rafId) return; - - const deltaX = e.clientX - initialMouseX; - const deltaY = e.clientY - initialMouseY; - - let shouldExpand = false; - - if (orientation === "horizontal") { - if (collapsedCorner.endsWith("left") && deltaX > DRAG_THRESHOLD) { - shouldExpand = true; - } else if ( - collapsedCorner.endsWith("right") && - deltaX < -DRAG_THRESHOLD - ) { - shouldExpand = true; - } - } else { - if (collapsedCorner.startsWith("top") && deltaY > DRAG_THRESHOLD) { - shouldExpand = true; - } else if ( - collapsedCorner.startsWith("bottom") && - deltaY < -DRAG_THRESHOLD - ) { - shouldExpand = true; - } - } - - if (shouldExpand) { - hasExpanded = true; - - signalWidgetCollapsed.value = null; - saveLocalStorage(LOCALSTORAGE_COLLAPSED_KEY, null); - - if (refInitialMinimizedWidth.current === 0 && refWidget.current) { - requestAnimationFrame(() => { - if (refWidget.current) { - refWidget.current.style.width = "min-content"; - const naturalWidth = refWidget.current.offsetWidth; - refInitialMinimizedWidth.current = naturalWidth || 300; - - const lastDims = signalWidget.value.lastDimensions; - const targetWidth = calculateBoundedSize( - lastDims.width, - 0, - true - ); - const targetHeight = calculateBoundedSize( - lastDims.height, - 0, - false - ); - - let newX = e.clientX - targetWidth / 2; - let newY = e.clientY - targetHeight / 2; - - const safeArea = getSafeArea(); - newX = Math.max( - safeArea.left, - Math.min(newX, window.innerWidth - targetWidth - safeArea.right) - ); - newY = Math.max( - safeArea.top, - Math.min(newY, window.innerHeight - targetHeight - safeArea.bottom) - ); - - signalWidget.value = { - ...signalWidget.value, - dimensions: { - ...signalWidget.value.dimensions, - position: { x: newX, y: newY }, - }, - }; - - updateWidgetPosition(true); - - const savedView = readLocalStorage( - LOCALSTORAGE_LAST_VIEW_KEY - ); - signalWidgetViews.value = savedView || { view: "none" }; - - setTimeout(() => { - if (refWidget.current) { - const dragEvent = new PointerEvent("pointerdown", { - clientX: e.clientX, - clientY: e.clientY, - pointerId: e.pointerId, - bubbles: true, - }); - refWidget.current.dispatchEvent(dragEvent); - } - }, 100); - } - }); - } else { - updateWidgetPosition(true); - const savedView = readLocalStorage( - LOCALSTORAGE_LAST_VIEW_KEY - ); - signalWidgetViews.value = savedView || { view: "none" }; - } - - document.removeEventListener("pointermove", handlePointerMove); - document.removeEventListener("pointerup", handlePointerEnd); - } - }; - - const handlePointerEnd = () => { - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - document.removeEventListener("pointermove", handlePointerMove); - document.removeEventListener("pointerup", handlePointerEnd); - }; - - document.addEventListener("pointermove", handlePointerMove); - document.addEventListener("pointerup", handlePointerEnd); + onPositionUpdate: setToolbarPosition, + onSnapEdgeChange: (edge, ratio) => { + updateToolbarState((state) => ({ + ...state, + edge, + ratio, + })); + updatePanelCorner(edge, ratio); }, - [] - ); - - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - if (!refWidget.current) return; - - removeLocalStorage(LOCALSTORAGE_LAST_VIEW_KEY); + onSnapComplete: (edge, ratio, position) => { + setToolbarPosition(position); + updateToolbarState((state) => ({ + ...state, + edge, + ratio, + })); + }, + }); - if (!signalWidgetCollapsed.value) { - refWidget.current.style.width = "min-content"; - refInitialMinimizedHeight.current = 36; // height of the header - refInitialMinimizedWidth.current = refWidget.current.offsetWidth; - } else { - refInitialMinimizedHeight.current = 36; - refInitialMinimizedWidth.current = 0; + const setCollapsed = (collapsed: boolean) => { + updateToolbarState((state) => ({ ...state, collapsed })); + if (collapsed) { + setWidgetView({ view: "none" }); } + }; - const safeArea = getSafeArea(); - refWidget.current.style.maxWidth = `calc(100vw - ${safeArea.left + safeArea.right}px)`; - refWidget.current.style.maxHeight = `calc(100vh - ${safeArea.top + safeArea.bottom}px)`; - - updateWidgetPosition(); + const handleWindowResize = () => { + cancelAnimationFrame(resizeFrameId ?? 0); + resizeFrameId = requestAnimationFrame(() => { + updateExpandedPosition(); + updatePanelCorner(getToolbarState().edge, getToolbarState().ratio); + }); + }; - if ( - Store.inspectState.value.kind !== "focused" && - !signalWidgetCollapsed.value && - !refExpandingFromCollapsed.current - ) { - signalWidget.value = { - ...signalWidget.value, - dimensions: { - isFullWidth: false, - isFullHeight: false, - width: refInitialMinimizedWidth.current, - height: refInitialMinimizedHeight.current, - position: signalWidget.value.dimensions.position, - }, - }; + onMount(() => { + if (panelContainer) { + setWidgetRef(panelContainer); + const safeArea = getSafeArea(); + panelContainer.style.maxWidth = `calc(100vw - ${safeArea.left + safeArea.right}px)`; + panelContainer.style.maxHeight = `calc(100vh - ${safeArea.top + safeArea.bottom}px)`; } - signalRefWidget.value = refWidget.current; - - const unsubscribeSignalWidget = signalWidget.subscribe((widget) => { - if (!refWidget.current) return; - - const { x, y } = widget.dimensions.position; - const { width, height } = widget.dimensions; - const container = refWidget.current; - - requestAnimationFrame(() => { - container.style.transform = `translate3d(${x}px, ${y}px, 0)`; - container.style.width = `${width}px`; - container.style.height = `${height}px`; - }); + measureExpandedToolbar(); + updatePanelCorner(getToolbarState().edge, getToolbarState().ratio); + fadeTimeoutId = setTimeout(() => setIsLoaded(true), TOOLBAR_FADE_IN_DELAY_MS); + window.addEventListener("resize", handleWindowResize, { + passive: true, }); - - const unsubscribeSignalWidgetViews = signalWidgetViews.subscribe( - (state) => { - refShouldOpen.current = state.view !== "none"; - updateWidgetPosition(); - - if (!signalWidgetCollapsed.value) { - if (state.view !== "none") { - saveLocalStorage(LOCALSTORAGE_LAST_VIEW_KEY, state); - } else { - removeLocalStorage(LOCALSTORAGE_LAST_VIEW_KEY); - } - } - } - ); - - const unsubscribeStoreInspectState = Store.inspectState.subscribe( - (state) => { - refShouldOpen.current = state.kind === "focused"; - updateWidgetPosition(); - } - ); - - const handleWindowResize = () => { - updateWidgetPosition(true); + window.visualViewport?.addEventListener("resize", handleWindowResize, { + passive: true, + }); + window.visualViewport?.addEventListener("scroll", handleWindowResize, { + passive: true, + }); + }); + + createEffect(() => { + if (isCollapsed()) return; + requestAnimationFrame(measureExpandedToolbar); + }); + + onCleanup(() => { + clearTimeout(fadeTimeoutId); + cancelAnimationFrame(resizeFrameId ?? 0); + window.removeEventListener("resize", handleWindowResize); + window.visualViewport?.removeEventListener("resize", handleWindowResize); + window.visualViewport?.removeEventListener("scroll", handleWindowResize); + setWidgetRef(null); + savePanelState(); + }); + + const toolbarStyle = () => { + const position = visibleToolbarPosition(); + const dimensions = collapsedDimensions(); + return { + width: isCollapsed() ? `${dimensions.width}px` : "max-content", + height: isCollapsed() ? `${dimensions.height}px` : "max-content", + opacity: isLoaded() ? 1 : 0, + transform: `translate3d(${position.x}px, ${position.y}px, 0)`, + transition: + toolbarDrag.isDragging() && !toolbarDrag.isSnapping() + ? "none" + : `transform ${TOOLBAR_SNAP_ANIMATION_DURATION_MS}ms var(--rs-ease-drawer), width ${TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS}ms var(--rs-ease-drawer), height ${TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS}ms var(--rs-ease-drawer), opacity ${TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS}ms ease-out`, + "z-index": TOOLBAR_Z_INDEX, }; - - window.addEventListener("resize", handleWindowResize, { passive: true }); - - return () => { - window.removeEventListener("resize", handleWindowResize); - unsubscribeSignalWidgetViews(); - unsubscribeStoreInspectState(); - unsubscribeSignalWidget(); - - saveLocalStorage(LOCALSTORAGE_KEY, { - ...getDefaultWidgetConfig(), - corner: signalWidget.value.corner, - }); + }; + + const panelStyle = () => { + const dimensions = getWidgetState().dimensions; + return { + width: `${dimensions.width}px`, + height: `${dimensions.height}px`, + transform: `translate3d(${dimensions.position.x}px, ${dimensions.position.y}px, 0)`, + "z-index": TOOLBAR_Z_INDEX - 1, }; - }, []); - - // i don't want to put the ref in state, so this is the solution to force context to propagate it - const [_, setTriggerRender] = useState(false); - useEffect(() => { - setTriggerRender(true); - }, []); - - const isCollapsed = signalWidgetCollapsed.value; - - let arrowRotationClass = ""; - if (isCollapsed) { - const { orientation = "horizontal", corner } = isCollapsed; - if (orientation === "horizontal") { - arrowRotationClass = corner?.endsWith("right") ? "rotate-180" : ""; - } else { - arrowRotationClass = corner?.startsWith("bottom") - ? "-rotate-90" - : "rotate-90"; - } - } + }; return ( <> - -
{ - const { orientation = "horizontal", corner } = isCollapsed; - if (orientation === "horizontal") { - return corner?.endsWith("right") - ? "rounded-tl-lg rounded-bl-lg shadow-lg" - : "rounded-tr-lg rounded-br-lg shadow-lg"; - } else { - return corner?.startsWith("bottom") - ? "rounded-tl-lg rounded-tr-lg shadow-lg" - : "rounded-bl-lg rounded-br-lg shadow-lg"; - } - })() - : "rounded-lg shadow-lg", - "flex flex-col", - "font-mono text-[13px]", - "user-select-none", - "opacity-0", - isCollapsed ? "cursor-pointer" : "cursor-move", - "z-[124124124124]", - "animate-fade-in animation-duration-300 animation-delay-300", - "will-change-transform", - "[touch-action:none]" - )} - style={{ WebkitAppRegion: "no-drag" }} +
setIsToolbarHovered(true)} + onMouseLeave={() => setIsToolbarHovered(false)} + class="fixed left-0 top-0 select-none font-sans text-[13px] [touch-action:none] [will-change:transform]" + classList={{ + "cursor-grabbing": toolbarDrag.isDragging(), + "cursor-grab": !toolbarDrag.isDragging() && !isCollapsed(), + }} + style={toolbarStyle()} + > + setCollapsed(false))} + /> + } > - {/* this entire feature is vibe coded don't think too hard about the code its probably very non coherent */} - {isCollapsed ? ( - - ) : ( - <> - - - - - - - )} -
- + setCollapsed(true))} + /> + +
+ +
+ + + + + + + +
); }; - -export const ToolbarElementContext = createContext(null); diff --git a/packages/scan/src/web/widget/resize-handle.tsx b/packages/scan/src/web/widget/resize-handle.tsx index a571654d..ac81a7ea 100644 --- a/packages/scan/src/web/widget/resize-handle.tsx +++ b/packages/scan/src/web/widget/resize-handle.tsx @@ -1,18 +1,9 @@ -import type { JSX } from 'preact'; -import { useCallback, useEffect, useRef } from 'preact/hooks'; -import { Store } from '~core/index'; -import { Icon } from '~web/components/icon'; -import { - LOCALSTORAGE_KEY, - MIN_CONTAINER_WIDTH, - MIN_SIZE, -} from '~web/constants'; -import { - signalRefWidget, - signalWidget, - signalWidgetViews, -} from '~web/state'; -import { cn, saveLocalStorage } from '~web/utils/helpers'; +import { createEffect, onCleanup } from "solid-js"; +import { getInspectState } from "../../core/native-state"; +import { Icon } from "../components/icon"; +import { LOCALSTORAGE_KEY, MIN_CONTAINER_WIDTH, MIN_SIZE } from "../constants"; +import { getWidgetRef, getWidgetState, getWidgetView, setWidgetState } from "../state"; +import { cn, saveLocalStorage } from "../utils/helpers"; import { calculateNewSizeAndPosition, calculatePosition, @@ -20,355 +11,301 @@ import { getHandleVisibility, getOppositeCorner, getWindowDimensions, -} from './helpers'; -import type { Corner, ResizeHandleProps } from './types'; +} from "./helpers"; +import type { ResizeHandleProps } from "./types"; -export const ResizeHandle = ({ position }: ResizeHandleProps) => { - const refContainer = useRef(null); +export const ResizeHandle = (props: ResizeHandleProps) => { + let container: HTMLDivElement | undefined; + let resizeAbortController: AbortController | undefined; - const prevWidth = useRef(null); - const prevHeight = useRef(null); - const prevCorner = useRef(null); - - // oxlint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - const container = refContainer.current; + const updateVisibility = () => { if (!container) return; - const updateVisibility = () => { - container.classList.remove('pointer-events-none'); - - const isFocused = Store.inspectState.value.kind === 'focused'; - const shouldShow = signalWidgetViews.value.view !== 'none'; - const isVisible = - (isFocused || shouldShow) && - getHandleVisibility( - position, - signalWidget.value.corner, - signalWidget.value.dimensions.isFullWidth, - signalWidget.value.dimensions.isFullHeight, - ); - - if (isVisible) { - container.classList.remove( - 'hidden', - 'pointer-events-none', - 'opacity-0', - ); - } else { - container.classList.add('hidden', 'pointer-events-none', 'opacity-0'); - } - }; - - const unsubscribeSignalWidget = signalWidget.subscribe((state) => { - 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; - } - - updateVisibility(); - - prevWidth.current = state.dimensions.width; - prevHeight.current = state.dimensions.height; - prevCorner.current = state.corner; - }); - - const unsubscribeInspectState = Store.inspectState.subscribe(() => { - updateVisibility(); - }); - - return () => { - unsubscribeSignalWidget(); - unsubscribeInspectState(); - prevWidth.current = null; - prevHeight.current = null; - prevCorner.current = null; - }; - }, []); - - // oxlint-disable-next-line react-hooks/exhaustive-deps - const handleResize = useCallback( - (e: JSX.TargetedPointerEvent) => { - e.preventDefault(); - e.stopPropagation(); + container.classList.remove("pointer-events-none"); + + const widgetState = getWidgetState(); + const isFocused = getInspectState().kind === "focused"; + const shouldShow = getWidgetView().view !== "none"; + const isVisible = + (isFocused || shouldShow) && + getHandleVisibility( + props.position, + widgetState.corner, + widgetState.dimensions.isFullWidth, + widgetState.dimensions.isFullHeight, + ); - const widget = signalRefWidget.value; - if (!widget) return; + if (isVisible) { + container.classList.remove("hidden", "pointer-events-none", "opacity-0"); + } else { + container.classList.add("hidden", "pointer-events-none", "opacity-0"); + } + }; + + createEffect(updateVisibility); + onCleanup(() => resizeAbortController?.abort()); + + const handleResize = (event: PointerEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const widget = getWidgetRef(); + if (!widget) return; + + const containerStyle = widget.style; + const { dimensions } = getWidgetState(); + const initialX = event.clientX; + const initialY = event.clientY; + + const initialWidth = dimensions.width; + const initialHeight = dimensions.height; + const initialPosition = dimensions.position; + + setWidgetState((state) => ({ + ...state, + dimensions: { + ...dimensions, + isFullWidth: false, + isFullHeight: false, + width: initialWidth, + height: initialHeight, + position: initialPosition, + }, + })); + + const pointerId = event.pointerId; + let latestClientX = initialX; + let latestClientY = initialY; + let resizeFrameId: number | undefined; + + const applyResize = () => { + const { newSize, newPosition } = calculateNewSizeAndPosition( + props.position, + { width: initialWidth, height: initialHeight }, + initialPosition, + latestClientX - initialX, + latestClientY - initialY, + ); - const containerStyle = widget.style; - const { dimensions } = signalWidget.value; - const initialX = e.clientX; - const initialY = e.clientY; + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + containerStyle.width = `${newSize.width}px`; + containerStyle.height = `${newSize.height}px`; - const initialWidth = dimensions.width; - const initialHeight = dimensions.height; - const initialPosition = dimensions.position; + const maxTreeWidth = Math.floor(newSize.width - MIN_CONTAINER_WIDTH / 2); + const currentTreeWidth = getWidgetState().componentsTree.width; + const newTreeWidth = Math.min(maxTreeWidth, Math.max(MIN_CONTAINER_WIDTH, currentTreeWidth)); - signalWidget.value = { - ...signalWidget.value, + setWidgetState((state) => ({ + ...state, dimensions: { - ...dimensions, isFullWidth: false, isFullHeight: false, - width: initialWidth, - height: initialHeight, - position: initialPosition, + width: newSize.width, + height: newSize.height, + position: newPosition, }, - }; - - let rafId: number | null = null; - - const handlePointerMove = (e: PointerEvent) => { - 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`; - - // Adjust components tree width when widget is resized - const maxTreeWidth = Math.floor(newSize.width - (MIN_CONTAINER_WIDTH / 2)); - const currentTreeWidth = signalWidget.value.componentsTree.width; - const newTreeWidth = Math.min( - maxTreeWidth, - Math.max(MIN_CONTAINER_WIDTH, currentTreeWidth), - ); - - signalWidget.value = { - ...signalWidget.value, - dimensions: { - isFullWidth: false, - isFullHeight: false, - width: newSize.width, - height: newSize.height, - position: newPosition, - }, - componentsTree: { - ...signalWidget.value.componentsTree, - width: newTreeWidth, - }, - }; - - rafId = null; - }); - }; - - const handlePointerUp = () => { - if (rafId) { - cancelAnimationFrame(rafId); - rafId = null; - } - document.removeEventListener('pointermove', handlePointerMove); - document.removeEventListener('pointerup', handlePointerUp); - - 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 = () => { - widget.removeEventListener('transitionend', onTransitionEnd); - }; - - widget.addEventListener('transitionend', onTransitionEnd); - containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; - - signalWidget.value = { - ...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, - componentsTree: signalWidget.value.componentsTree, - }); - }; - - document.addEventListener('pointermove', handlePointerMove, { - passive: true, - }); - document.addEventListener('pointerup', handlePointerUp); - }, - [], - ); + componentsTree: { + ...state.componentsTree, + width: newTreeWidth, + }, + })); - // oxlint-disable-next-line react-hooks/exhaustive-deps - const handleDoubleClick = useCallback( - (e: JSX.TargetedMouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + resizeFrameId = undefined; + }; - const widget = signalRefWidget.value; - if (!widget) return; + const handlePointerMove = (pointerEvent: PointerEvent) => { + latestClientX = pointerEvent.clientX; + latestClientY = pointerEvent.clientY; + containerStyle.transition = "none"; + if (resizeFrameId !== undefined) return; + resizeFrameId = requestAnimationFrame(applyResize); + }; - const containerStyle = widget.style; - const { dimensions, corner } = signalWidget.value; - const windowDims = getWindowDimensions(); + const handlePointerUp = () => { + if (resizeFrameId !== undefined) { + cancelAnimationFrame(resizeFrameId); + applyResize(); + } + resizeAbortController?.abort(); + resizeAbortController = undefined; + if (container?.hasPointerCapture(pointerId)) { + container.releasePointerCapture(pointerId); + } + const { dimensions, corner } = getWidgetState(); + 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, - ); - 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.initialHeight - : windowDims.maxHeight; - } + let newCorner = corner; + if (isFullScreen || isCurrentFullWidth || isCurrentFullHeight) { + newCorner = getClosestCorner(dimensions.position); } - if (isFullScreen) { - if (position === 'left' || position === 'right') { - newWidth = MIN_SIZE.width; - } else { - newHeight = MIN_SIZE.initialHeight; - } - } + const newPosition = calculatePosition(newCorner, dimensions.width, dimensions.height); - const newPosition = calculatePosition(newCorner, newWidth, newHeight); - const newDimensions = { - isFullWidth: windowDims.isFullWidth(newWidth), - isFullHeight: windowDims.isFullHeight(newHeight), - width: newWidth, - height: newHeight, - position: newPosition, - }; - - // Adjust components tree width when widget is resized - const maxTreeWidth = Math.floor(newWidth - MIN_SIZE.width / 2); - const currentTreeWidth = signalWidget.value.componentsTree.width; - const defaultWidth = Math.floor(newWidth * 0.3); // Use 30% of window width as default - - const newTreeWidth = isCurrentFullWidth - ? MIN_CONTAINER_WIDTH - : (position === 'left' || position === 'right') && !isCurrentFullWidth - ? Math.min(maxTreeWidth, Math.max(MIN_CONTAINER_WIDTH, defaultWidth)) - : Math.min( - maxTreeWidth, - Math.max(MIN_CONTAINER_WIDTH, currentTreeWidth), - ); - - requestAnimationFrame(() => { - signalWidget.value = { - corner: newCorner, - dimensions: newDimensions, - lastDimensions: dimensions, - componentsTree: { - ...signalWidget.value.componentsTree, - width: newTreeWidth, - }, - }; - - containerStyle.transition = 'all 0.25s cubic-bezier(0, 0, 0.2, 1)'; - containerStyle.width = `${newWidth}px`; - containerStyle.height = `${newHeight}px`; - containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; - }); + containerStyle.transform = `translate3d(${newPosition.x}px, ${newPosition.y}px, 0)`; + + setWidgetState((state) => ({ + ...state, + 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, + }, + })); + const widgetState = getWidgetState(); saveLocalStorage(LOCALSTORAGE_KEY, { + corner: newCorner, + dimensions: widgetState.dimensions, + lastDimensions: widgetState.lastDimensions, + componentsTree: widgetState.componentsTree, + }); + }; + + container?.setPointerCapture(pointerId); + resizeAbortController?.abort(); + resizeAbortController = new AbortController(); + const listenerOptions = { signal: resizeAbortController.signal }; + window.addEventListener("pointermove", handlePointerMove, listenerOptions); + window.addEventListener("pointerup", handlePointerUp, listenerOptions); + window.addEventListener("pointercancel", handlePointerUp, listenerOptions); + }; + + const handleDoubleClick = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const widget = getWidgetRef(); + if (!widget) return; + + const containerStyle = widget.style; + const { dimensions, corner } = getWidgetState(); + 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( + props.position, + corner, + isFullScreen, + isCurrentFullWidth, + isCurrentFullHeight, + ); + + if (props.position === "left" || props.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.initialHeight : windowDims.maxHeight; + } + } + + if (isFullScreen) { + if (props.position === "left" || props.position === "right") { + newWidth = MIN_SIZE.width; + } else { + newHeight = MIN_SIZE.initialHeight; + } + } + + const newPosition = calculatePosition(newCorner, newWidth, newHeight); + const newDimensions = { + isFullWidth: windowDims.isFullWidth(newWidth), + isFullHeight: windowDims.isFullHeight(newHeight), + width: newWidth, + height: newHeight, + position: newPosition, + }; + + // Adjust components tree width when widget is resized + const maxTreeWidth = Math.floor(newWidth - MIN_SIZE.width / 2); + const currentTreeWidth = getWidgetState().componentsTree.width; + const defaultWidth = Math.floor(newWidth * 0.3); // Use 30% of window width as default + + const newTreeWidth = isCurrentFullWidth + ? MIN_CONTAINER_WIDTH + : (props.position === "left" || props.position === "right") && !isCurrentFullWidth + ? Math.min(maxTreeWidth, Math.max(MIN_CONTAINER_WIDTH, defaultWidth)) + : Math.min(maxTreeWidth, Math.max(MIN_CONTAINER_WIDTH, currentTreeWidth)); + + requestAnimationFrame(() => { + setWidgetState((state) => ({ corner: newCorner, dimensions: newDimensions, lastDimensions: dimensions, componentsTree: { - ...signalWidget.value.componentsTree, + ...state.componentsTree, width: newTreeWidth, }, - }); - }, - [], - ); + })); + + containerStyle.transition = "all 0.25s cubic-bezier(0, 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, + componentsTree: { + ...getWidgetState().componentsTree, + width: newTreeWidth, + }, + }); + }; return (
- - + + diff --git a/packages/scan/src/web/widget/types.ts b/packages/scan/src/web/widget/types.ts index 4b6a40c8..912efb75 100644 --- a/packages/scan/src/web/widget/types.ts +++ b/packages/scan/src/web/widget/types.ts @@ -9,11 +9,13 @@ export interface Size { } export type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right"; +export type SnapEdge = "top" | "bottom" | "left" | "right"; -export type CollapsedPosition = { - corner: Corner; - orientation: "horizontal" | "vertical"; -}; +export interface ToolbarState { + edge: SnapEdge; + ratio: number; + collapsed: boolean; +} export interface ResizeHandleProps { position: Corner | "top" | "bottom" | "left" | "right"; diff --git a/packages/scan/tsconfig.json b/packages/scan/tsconfig.json index fba77e1f..d300ca03 100644 --- a/packages/scan/tsconfig.json +++ b/packages/scan/tsconfig.json @@ -4,19 +4,14 @@ "ignoreDeprecations": "6.0", "rootDir": "src", "outDir": "dist", - "baseUrl": ".", "module": "ESNext", "moduleResolution": "bundler", "declaration": true, "declarationMap": true, "sourceMap": true, - "jsx": "react-jsx", - "jsxImportSource": "preact", - "types": ["node"], - "paths": { - "~web/*": ["src/web/*"], - "~core/*": ["src/core/*"] - } + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node"] }, "include": ["src", "global.d.ts"], "exclude": ["node_modules", "dist"] diff --git a/packages/scan/tsup.config.ts b/packages/scan/tsup.config.ts deleted file mode 100644 index e53c18a7..00000000 --- a/packages/scan/tsup.config.ts +++ /dev/null @@ -1,246 +0,0 @@ -import * as fs from "node:fs"; -import fsPromise from "node:fs/promises"; -import path from "node:path"; -import { TsconfigPathsPlugin } from "@esbuild-plugins/tsconfig-paths"; -import { init, parse } from "es-module-lexer"; -import { defineConfig } from "tsup"; -import { workerPlugin } from "./worker-plugin"; - -const DIST_PATH = "./dist"; - -const USE_CLIENT_DIRECTIVE = `'use client';\n`; - -const addDirectivesToChunkFiles = async (readPath: string): Promise => { - try { - if (!fs.existsSync(readPath)) return; - const files = await fsPromise.readdir(readPath); - for (const file of files) { - if (file.endsWith(".mjs") || file.endsWith(".js")) { - const filePath = path.join(readPath, file); - const data = await fsPromise.readFile(filePath, "utf8"); - if (data.startsWith(USE_CLIENT_DIRECTIVE)) continue; - await fsPromise.writeFile(filePath, `${USE_CLIENT_DIRECTIVE}${data}`, "utf8"); - } - } - } catch (err) { - // oxlint-disable-next-line no-console - console.error("Error:", err); - } -}; - -const banner = `/** - * Copyright 2025 Aiden Bai, Million Software, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the “Software”), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, - * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */`; - -void (async () => { - await init; - - if (fs.existsSync(DIST_PATH)) { - fs.rmSync(DIST_PATH, { recursive: true }); - } - fs.mkdirSync(DIST_PATH, { recursive: true }); - - const code = fs.readFileSync("./src/core/index.ts", "utf8"); - const [_, allExports] = parse(code); - const names: Array = []; - for (const exportItem of allExports) { - names.push(exportItem.n); - } - - const createFn = (name: string) => `export let ${name}=()=>{}`; - const createVar = (name: string) => `export let ${name}=undefined`; - - let script = ""; - for (const name of names) { - if (name[0].toLowerCase() === name[0]) { - script += `${createFn(name)}\n`; - continue; - } - script += `${createVar(name)}\n`; - } - - setTimeout(() => { - for (const ext of ["js", "mjs", "global.js"]) { - fs.writeFileSync(`./dist/rsc-shim.${ext}`, script); - } - }, 500); -})(); - -export default defineConfig([ - { - entry: ["./src/auto.ts", "./src/install-hook.ts"], - outDir: DIST_PATH, - banner: { - js: banner, - }, - splitting: false, - clean: false, - sourcemap: false, - format: ["iife"], - // Target ES2019 (no `?.`, no `??`) so older babel-loader configs without - // `@babel/preset-env` for optional chaining can still parse the bundle - // (#287, #336). - target: "es2019", - platform: "browser", - treeshake: true, - dts: true, - minify: process.env.NODE_ENV === "production" ? "terser" : false, - env: { - NODE_ENV: process.env.NODE_ENV ?? "development", - }, - external: [ - "react", - "react-dom", - "next", - "next/navigation", - "react-router", - "react-router-dom", - "@remix-run/react", - ], - esbuildPlugins: [workerPlugin], - loader: { - ".css": "text", - ".worker.js": "text", - }, - }, - { - entry: [ - "./src/index.ts", - "./src/install-hook.ts", - "./src/core/all-environments.ts", - "./src/lite/index.ts", - ], - banner: { - js: banner, - }, - outDir: DIST_PATH, - splitting: false, - clean: false, - sourcemap: false, - format: ["cjs", "esm"], - target: "es2019", - platform: "browser", - // FIXME: tree shaking removes use client directive - // Info: vercel analytics does the same thing- https://github.com/vercel/analytics/blob/main/packages/web/tsup.config.js - treeshake: false, - dts: true, - watch: process.env.NODE_ENV === "development", - async onSuccess() { - await addDirectivesToChunkFiles(DIST_PATH); - await addDirectivesToChunkFiles(path.join(DIST_PATH, "lite")); - await addDirectivesToChunkFiles(path.join(DIST_PATH, "core")); - }, - minify: false, - env: { - NODE_ENV: process.env.NODE_ENV ?? "development", - NPM_PACKAGE_VERSION: JSON.parse( - fs.readFileSync(path.join(__dirname, "../scan", "package.json"), "utf8"), - ).version, - }, - external: [ - "react", - "react-dom", - "next", - "next/navigation", - "react-router", - "react-router-dom", - "@remix-run/react", - "preact", - "@preact/signals", - ], - loader: { - ".css": "text", - }, - esbuildPlugins: [ - workerPlugin, - TsconfigPathsPlugin({ - tsconfig: path.resolve(__dirname, "./tsconfig.json"), - }), - ], - }, - { - entry: ["./src/cli.mts"], - outDir: DIST_PATH, - banner: { - js: banner, - }, - splitting: false, - clean: false, - sourcemap: false, - format: ["cjs"], - target: "esnext", - platform: "node", - minify: false, - env: { - NODE_ENV: process.env.NODE_ENV ?? "development", - NPM_PACKAGE_VERSION: JSON.parse( - fs.readFileSync(path.join(__dirname, "../scan", "package.json"), "utf8"), - ).version, - }, - watch: process.env.NODE_ENV === "development", - }, - { - entry: [ - "./src/react-component-name/index.ts", - "./src/react-component-name/vite.ts", - "./src/react-component-name/webpack.ts", - "./src/react-component-name/esbuild.ts", - "./src/react-component-name/rspack.ts", - "./src/react-component-name/rolldown.ts", - "./src/react-component-name/rollup.ts", - "./src/react-component-name/astro.ts", - "./src/react-component-name/loader.ts", - ], - outDir: `${DIST_PATH}/react-component-name`, - splitting: false, - sourcemap: false, - clean: false, - format: ["cjs", "esm"], - target: "esnext", - external: [ - "unplugin", - "estree-walker", - "@rollup/pluginutils", - "@babel/types", - "@babel/parser", - "@babel/traverse", - "@babel/generator", - "@babel/core", - "rollup", - "webpack", - "esbuild", - "rspack", - "vite", - ], - dts: true, - minify: false, - treeshake: true, - env: { - NODE_ENV: process.env.NODE_ENV || "development", - }, - outExtension: ({ format }) => ({ - js: format === "esm" ? ".mjs" : ".js", - }), - esbuildOptions: (options, context) => { - options.mainFields = ["module", "main"]; - options.conditions = ["import", "require", "node", "default"]; - options.format = context.format === "esm" ? "esm" : "cjs"; - options.preserveSymlinks = true; - }, - }, -]); diff --git a/packages/scan/vite.config.mts b/packages/scan/vite.config.mts index 18ea2ce8..23d26907 100644 --- a/packages/scan/vite.config.mts +++ b/packages/scan/vite.config.mts @@ -1,7 +1,277 @@ -import { defineConfig } from "vitest/config"; +import { rmSync } from "node:fs"; +import { defineConfig } from "vite-plus"; +import type { Plugin } from "vite-plus"; +import type { PackUserConfig } from "vite-plus/pack"; +import packageJson from "./package.json"; +import { + cssTextPlugin, + declarationsOnlyPlugin, + solidVitePlugin, + solidWebBrowserPlugin, + workerCodePlugin, +} from "./solid-vite-plugin"; -export default defineConfig({ - resolve: { - tsconfigPaths: true, +const DIST_PATH = "./dist"; +const BROWSER_TARGET = "es2019"; +const USE_CLIENT_DIRECTIVE = "'use client';"; +const IS_COVERAGE_BUILD = process.env.REACT_SCAN_COVERAGE === "true"; +let hasCleanedDist = false; + +const licenseBanner = `/** + * Copyright 2025 Aiden Bai, Million Software, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */`; + +const browserExternalDependencies = [ + "react", + "react-dom", + "next", + "next/navigation", + "react-router", + "react-router-dom", + "@remix-run/react", +]; + +const reactComponentNameExternalDependencies = [ + "unplugin", + "estree-walker", + "@rollup/pluginutils", + "@babel/types", + "@babel/parser", + "@babel/traverse", + "@babel/generator", + "@babel/core", + "rollup", + "webpack", + "esbuild", + "rspack", + "vite", +]; + +const browserModuleEntries = { + index: "./src/index.ts", + auto: "./src/auto.ts", + "install-hook": "./src/install-hook.ts", + "core/all-environments": "./src/core/all-environments.ts", + "lite/index": "./src/lite/index.ts", +}; + +const reactComponentNameEntries = { + "react-component-name/index": "./src/react-component-name/index.ts", + "react-component-name/vite": "./src/react-component-name/vite.ts", + "react-component-name/webpack": "./src/react-component-name/webpack.ts", + "react-component-name/esbuild": "./src/react-component-name/esbuild.ts", + "react-component-name/rspack": "./src/react-component-name/rspack.ts", + "react-component-name/rolldown": "./src/react-component-name/rolldown.ts", + "react-component-name/rollup": "./src/react-component-name/rollup.ts", + "react-component-name/astro": "./src/react-component-name/astro.ts", + "react-component-name/loader": "./src/react-component-name/loader.ts", +}; + +const declarationEntries = { + index: "./src/core/index.ts", + "install-hook": "./src/install-hook.ts", + "core/all-environments": "./src/core/all-environments.ts", + "lite/index": "./src/lite/index.ts", + "rsc-shim": "./src/rsc-shim.ts", + ...reactComponentNameEntries, +}; + +const createSharedPlugins = () => [solidWebBrowserPlugin(), cssTextPlugin(), solidVitePlugin()]; + +const cleanDistPlugin = (): Plugin => ({ + name: "clean-dist", + buildStart() { + if (hasCleanedDist || process.env.NODE_ENV === "development") return; + rmSync(DIST_PATH, { force: true, recursive: true }); + hasCleanedDist = true; }, }); + +const moduleOutExtensions: NonNullable = ({ format }) => ({ + js: format === "es" ? ".mjs" : ".js", + dts: format === "es" ? ".d.mts" : ".d.ts", +}); + +const createIifePack = (name: string, entry: string): PackUserConfig => ({ + entry: { [name]: entry }, + outDir: DIST_PATH, + format: ["iife"], + dts: false, + clean: false, + hash: false, + platform: "browser", + target: BROWSER_TARGET, + sourcemap: IS_COVERAGE_BUILD, + minify: process.env.NODE_ENV === "production" && !IS_COVERAGE_BUILD, + banner: licenseBanner, + env: { + NODE_ENV: process.env.NODE_ENV ?? "development", + }, + loader: { + ".css": "text", + }, + deps: { + neverBundle: browserExternalDependencies, + alwaysBundle: [/^bippy(?:\/|$)/, /^react-grab(?:\/|$)/, /^solid-js(?:\/|$)/], + onlyBundle: false, + }, + outputOptions: { + entryFileNames: "[name].global.js", + globals: { + react: "React", + }, + }, + plugins: [cleanDistPlugin(), ...createSharedPlugins(), workerCodePlugin()], +}); + +const createBrowserModulePack = (name: string, entry: string): PackUserConfig => ({ + entry: { [name]: entry }, + outDir: DIST_PATH, + format: ["cjs", "esm"], + dts: false, + clean: false, + hash: false, + platform: "browser", + target: BROWSER_TARGET, + sourcemap: IS_COVERAGE_BUILD, + minify: false, + treeshake: false, + banner: `${USE_CLIENT_DIRECTIVE}\n${licenseBanner}`, + env: { + NODE_ENV: process.env.NODE_ENV ?? "development", + NPM_PACKAGE_VERSION: packageJson.version, + }, + loader: { + ".css": "text", + }, + deps: { + neverBundle: browserExternalDependencies, + alwaysBundle: [/^bippy(?:\/|$)/, /^react-grab(?:\/|$)/, /^solid-js(?:\/|$)/], + onlyBundle: false, + }, + outExtensions: moduleOutExtensions, + outputOptions: { + codeSplitting: false, + }, + plugins: [cleanDistPlugin(), ...createSharedPlugins(), workerCodePlugin()], +}); + +const createReactComponentNamePack = (name: string, entry: string): PackUserConfig => ({ + entry: { [name]: entry }, + outDir: DIST_PATH, + format: ["cjs", "esm"], + dts: false, + clean: false, + hash: false, + platform: "node", + target: "esnext", + sourcemap: IS_COVERAGE_BUILD, + minify: false, + treeshake: true, + banner: licenseBanner, + env: { + NODE_ENV: process.env.NODE_ENV ?? "development", + }, + deps: { + neverBundle: reactComponentNameExternalDependencies, + }, + inputOptions: { + resolve: { + mainFields: ["module", "main"], + conditionNames: ["import", "require", "node", "default"], + symlinks: false, + }, + }, + outExtensions: moduleOutExtensions, + plugins: [cleanDistPlugin()], +}); + +const createDeclarationPack = (format: "cjs" | "esm"): PackUserConfig => ({ + entry: declarationEntries, + outDir: DIST_PATH, + format: [format], + dts: { + eager: true, + emitDtsOnly: true, + newContext: true, + resolver: "tsc", + sourcemap: false, + }, + clean: false, + hash: false, + platform: "neutral", + target: BROWSER_TARGET, + sourcemap: false, + outExtensions: moduleOutExtensions, + deps: { + onlyBundle: false, + }, + plugins: [cleanDistPlugin(), ...createSharedPlugins(), declarationsOnlyPlugin()], +}); + +export default defineConfig({ + plugins: createSharedPlugins(), + pack: [ + createIifePack("auto", "./src/auto.ts"), + createIifePack("install-hook", "./src/install-hook.ts"), + ...Object.entries(browserModuleEntries).map(([name, entry]) => + createBrowserModulePack(name, entry), + ), + { + entry: { "rsc-shim": "./src/rsc-shim.ts" }, + outDir: DIST_PATH, + format: ["cjs", "esm"], + dts: false, + clean: false, + hash: false, + platform: "neutral", + target: BROWSER_TARGET, + sourcemap: IS_COVERAGE_BUILD, + minify: false, + treeshake: true, + banner: licenseBanner, + outExtensions: moduleOutExtensions, + plugins: [cleanDistPlugin()], + }, + createDeclarationPack("cjs"), + createDeclarationPack("esm"), + { + entry: { cli: "./src/cli.mts" }, + outDir: DIST_PATH, + format: ["cjs"], + dts: false, + clean: false, + hash: false, + fixedExtension: false, + platform: "node", + target: "esnext", + sourcemap: IS_COVERAGE_BUILD, + minify: false, + banner: licenseBanner, + env: { + NODE_ENV: process.env.NODE_ENV ?? "development", + NPM_PACKAGE_VERSION: packageJson.version, + }, + outExtensions: () => ({ js: ".js" }), + plugins: [cleanDistPlugin()], + }, + ...Object.entries(reactComponentNameEntries).map(([name, entry]) => + createReactComponentNamePack(name, entry), + ), + ], +}); diff --git a/packages/scan/worker-plugin.ts b/packages/scan/worker-plugin.ts deleted file mode 100644 index 32dda5d4..00000000 --- a/packages/scan/worker-plugin.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as esbuild from 'esbuild'; - -/** - * A hacky plugin to build the worker file (resolving all imports), and inline - * the javascript into a variable by replacing __WORKER_CODE__ string in bundle with the worker - * build output - */ -export const workerPlugin = { - name: 'worker-plugin', - setup(build) { - const workerResult = esbuild.buildSync({ - entryPoints: ['src/new-outlines/offscreen-canvas.worker.ts'], - bundle: true, - write: false, - format: 'iife', - platform: 'browser', - minify: true, - }); - const workerCode = workerResult.outputFiles[0].text; - - build.onEnd((result) => { - if (!result.outputFiles) return; - - for (const file of result.outputFiles) { - const newText = file.text.replace( - 'var workerCode = "__WORKER_CODE__"', - `var workerCode = ${JSON.stringify(workerCode)}`, - ); - file.contents = Buffer.from(newText); - } - }); - }, -}; diff --git a/packages/website/public/auto.global.js b/packages/website/public/auto.global.js index 33d2f266..c8eaff8a 100644 --- a/packages/website/public/auto.global.js +++ b/packages/website/public/auto.global.js @@ -1,39 +1,3208 @@ -'use client'; -!function(e){"use strict";var t=Object.defineProperty,n=(e,n,r)=>((e,n,r)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[n]=r)(e,"symbol"!=typeof n?n+"":n,r);Array.prototype.toSorted||Object.defineProperty(Array.prototype,"toSorted",{value:function(e){return[...this].sort(e)},writable:!0,configurable:!0});var r,o="bippy-0.5.39",i=Object.defineProperty,a=Object.prototype.hasOwnProperty,s=()=>{},l=e=>{try{Function.prototype.toString.call(e).indexOf("^_^")>-1&&setTimeout(()=>{throw Error("React is running in production mode, but dead code elimination has not been applied. Read how to correctly configure React for production: https://reactjs.org/link/perf-use-production-build")})}catch{}},c=(e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__)=>!(!e||!("getFiberRoots"in e)),d=!1,u=(e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__)=>!!d||(e&&"function"==typeof e.inject&&(r=e.inject.toString()),!!(null==r?void 0:r.includes("(injected)"))),p=new Set,h=new Set,m=e=>{let t=new Map,n=0,r={_instrumentationIsActive:!1,_instrumentationSource:o,checkDCE:l,hasUnsupportedRendererAttached:!1,inject(e){let o=++n;return t.set(o,e),h.add(e),r._instrumentationIsActive||(r._instrumentationIsActive=!0,p.forEach(e=>e())),o},on:s,onCommitFiberRoot:s,onCommitFiberUnmount:s,onPostCommitFiberRoot:s,renderers:t,supportsFiber:!0,supportsFlight:!0};try{i(globalThis,"__REACT_DEVTOOLS_GLOBAL_HOOK__",{configurable:!0,enumerable:!0,get:()=>r,set(t){if(t&&"object"==typeof t){let n=r.renderers;r=t,n.size>0&&(n.forEach((e,n)=>{h.add(e),t.renderers.set(n,e)}),f(e))}}});let t=window.hasOwnProperty,n=!1;i(window,"hasOwnProperty",{configurable:!0,value:function(...e){try{if(!n&&"__REACT_DEVTOOLS_GLOBAL_HOOK__"===e[0])return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__=void 0,n=!0,-0}catch{}return t.apply(this,e)},writable:!0})}catch{f(e)}return r},f=e=>{e&&p.add(e);try{let t=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!t)return;if(!t._instrumentationSource){t.checkDCE=l,t.supportsFiber=!0,t.supportsFlight=!0,t.hasUnsupportedRendererAttached=!1,t._instrumentationSource=o,t._instrumentationIsActive=!1;let e=c(t);if(e||(t.on=s),t.renderers.size)return t._instrumentationIsActive=!0,void p.forEach(e=>e());let n=t.inject,r=u(t);r&&!e&&(d=!0,t.inject({scheduleRefresh(){}})&&(t._instrumentationIsActive=!0)),t.inject=e=>{let o=n(e);return h.add(e),r&&t.renderers.set(o,e),t._instrumentationIsActive=!0,p.forEach(e=>e()),o}}(t.renderers.size||t._instrumentationIsActive||u())&&(null==e||e())}catch{}},g=e=>a.call(globalThis,"__REACT_DEVTOOLS_GLOBAL_HOOK__")?(f(e),globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__):m(e);(()=>{try{typeof window<"u"&&((null==(e=window.document)?void 0:e.createElement)||"ReactNative"===(null==(t=window.navigator)?void 0:t.product))&&g()}catch{}var e,t})();var v=e=>{switch(e.tag){case 5:case 26:case 27:return!0;default:return"string"==typeof e.type}},w=e=>{switch(e.tag){case 1:case 11:case 0:case 14:case 15:return!0;default:return!1}},b=e=>{var t,n,r;let o=e.memoizedProps,i=(null==(t=e.alternate)?void 0:t.memoizedProps)||{},a=null!=(r=null!=(n=e.flags)?n:e.effectTag)?r:0;switch(e.tag){case 1:case 9:case 11:case 0:case 14:case 15:return!(1&~a);default:return!e.alternate||(i!==o||e.alternate.memoizedState!==e.memoizedState||e.alternate.ref!==e.ref)}},x=e=>!!(13374&e.flags||13374&e.subtreeFlags),y=e=>{switch(e.tag){case 18:case 7:case 6:case 23:case 22:return!0;case 3:return!1;default:{let t="object"==typeof e.type&&null!==e.type?e.type.$$typeof:e.type;switch("symbol"==typeof t?t.toString():t){case 60111:case"Symbol(react.concurrent_mode)":case"Symbol(react.async_mode)":return!0;default:return!1}}}};function k(e,t,n=!1){if(!e)return null;let r=t(e);if(r instanceof Promise)return(async()=>{if(!0===await r)return e;let o=n?e.return:e.child;for(;o;){let e=await B(o,t,n);if(e)return e;o=n?null:o.sibling}return null})();if(!0===r)return e;let o=n?e.return:e.child;for(;o;){let e=H(o,t,n);if(e)return e;o=n?null:o.sibling}return null}var _,N,S,C,T,E,z,A,M,$,F,R,O,D,j,L,P,I,W,U,H=(e,t,n=!1)=>{if(!e)return null;if(!0===t(e))return e;let r=n?e.return:e.child;for(;r;){let e=H(r,t,n);if(e)return e;r=n?null:r.sibling}return null},B=async(e,t,n=!1)=>{if(!e)return null;if(!0===await t(e))return e;let r=n?e.return:e.child;for(;r;){let e=await B(r,t,n);if(e)return e;r=n?null:r.sibling}return null},V=e=>{var t,n,r;let o=null!=(t=null==e?void 0:e.actualDuration)?t:0,i=o,a=null!=(n=null==e?void 0:e.child)?n:null;for(;o>0&&null!=a;)i-=null!=(r=a.actualDuration)?r:0,a=a.sibling;return{selfTime:i,totalTime:o}},q=e=>{var t;return!!(null==(t=e.updateQueue)?void 0:t.memoCache)},G=e=>{let t=e;return"function"==typeof t?t:"object"==typeof t&&t?G(t.type||t.render):null},J=e=>{let t=e;if("string"==typeof t)return t;if("function"!=typeof t&&("object"!=typeof t||!t))return null;let n=t.displayName||t.name||null;if(n)return n;let r=G(t);return r&&(r.displayName||r.name)||null},Y=e=>{try{if("string"==typeof e.version&&e.bundleType>0)return"development"}catch{}return"production"},X=0,K=new WeakMap,Z=e=>{let t=K.get(e);return!t&&e.alternate&&(t=K.get(e.alternate)),t||(t=X++,((e,t=X++)=>{K.set(e,t)})(e,t)),t},Q=(e,t,n)=>{let r=t;for(;null!=r;){if(K.has(r)||Z(r),!y(r)&&b(r)&&e(r,"mount"),13===r.tag)if(null!==r.memoizedState){let t=r.child,n=t?t.sibling:null;if(n){let t=n.child;null!==t&&Q(e,t,!1)}}else{let t=null;null!==r.child&&(t=r.child.child),null!==t&&Q(e,t,!1)}else null!=r.child&&Q(e,r.child,!0);r=n?r.sibling:null}},ee=(e,t,n,r)=>{var o,i,a,s,l,c;if(K.has(t)||Z(t),!n)return;K.has(n)||Z(n);let d=13===t.tag;!y(t)&&b(t)&&e(t,"update");let u=d&&null!==n.memoizedState,p=d&&null!==t.memoizedState;if(u&&p){let r=null!=(i=null==(o=t.child)?void 0:o.sibling)?i:null,l=null!=(s=null==(a=n.child)?void 0:a.sibling)?s:null;null!==r&&null!==l&&ee(e,r,l)}else if(u&&!p){let n=t.child;null!==n&&Q(e,n,!0)}else if(!u&&p){ne(e,n);let r=null!=(c=null==(l=t.child)?void 0:l.sibling)?c:null;null!==r&&Q(e,r,!0)}else if(t.child!==n.child){let n=t.child;for(;n;){if(n.alternate){let t=n.alternate;ee(e,n,t)}else Q(e,n,!1);n=n.sibling}}},te=(e,t)=>{(3===t.tag||!y(t))&&e(t,"unmount")},ne=(e,t)=>{var n,r,o,i;let a=13===t.tag&&null!==t.memoizedState,s=t.child;for(a&&(s=null!=(i=null==(o=null!=(r=null==(n=t.child)?void 0:n.sibling)?r:null)?void 0:o.child)?i:null);null!==s;)null!==s.return&&(te(e,s),ne(e,s)),s=s.sibling},re=0,oe=new WeakMap,ie="undefined"!=typeof window,ae={},se=[],le=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,ce=Array.isArray;function de(e,t){for(var n in t)e[n]=t[n];return e}function ue(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function pe(e,t,n){var r,o,i,a={};for(i in t)"key"==i?r=t[i]:"ref"==i?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?_.call(arguments,2):n),"function"==typeof e&&null!=e.defaultProps)for(i in e.defaultProps)void 0===a[i]&&(a[i]=e.defaultProps[i]);return he(e,a,r,o,null)}function he(e,t,n,r,o){var i={type:e,props:t,key:n,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==o?++S:o,__i:-1,__u:0};return null==o&&null!=N.vnode&&N.vnode(i),i}function me(e){return e.children}function fe(e,t){this.props=e,this.context=t}function ge(e,t){if(null==t)return e.__?ge(e.__,e.__i+1):null;for(var n;tt&&T.sort(A),e=T.shift(),t=T.length,ve(e)}finally{T.length=xe.__r=0}}function ye(e,t,n,r,o,i,a,s,l,c,d){var u,p,h,m,f,g,v,w=r&&r.__k||se,b=t.length;for(l=function(e,t,n,r,o){var i,a,s,l,c,d=n.length,u=d,p=0;for(e.__k=new Array(o),i=0;i0?a=e.__k[i]=he(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,l=i+p,a.__=e,a.__b=e.__b+1,s=null,-1!=(c=a.__i=Ne(a,n,l,u))&&(u--,(s=n[c])&&(s.__u|=2)),null==s||null==s.__v?(-1==c&&(o>d?p--:ol?p--:p++,a.__u|=4))):e.__k[i]=null;if(u)for(i=0;i(d?1:0))for(o=n-1,i=n+1;o>=0||i=0?o--:i++])&&!(2&c.__u)&&s==c.key&&l==c.type)return a;return-1}function Se(e,t,n){"-"==t[0]?e.setProperty(t,null==n?"":n):e[t]=null==n?"":"number"!=typeof n||le.test(t)?n:n+"px"}function Ce(e,t,n,r,o){var i,a;e:if("style"==t)if("string"==typeof n)e.style.cssText=n;else{if("string"==typeof r&&(e.style.cssText=r=""),r)for(t in r)n&&t in n||Se(e.style,t,"");if(n)for(t in n)r&&n[t]==r[t]||Se(e.style,t,n[t])}else if("o"==t[0]&&"n"==t[1])i=t!=(t=t.replace(R,"$1")),a=t.toLowerCase(),t=a in e||"onFocusOut"==t||"onFocusIn"==t?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=n,n?r?n[F]=r[F]:(n[F]=O,e.addEventListener(t,i?j:D,i)):e.removeEventListener(t,i?j:D,i);else{if("http://www.w3.org/2000/svg"==o)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!=t&&"height"!=t&&"href"!=t&&"list"!=t&&"form"!=t&&"tabIndex"!=t&&"download"!=t&&"rowSpan"!=t&&"colSpan"!=t&&"role"!=t&&"popover"!=t&&t in e)try{e[t]=null==n?"":n;break e}catch(e){}"function"==typeof n||(null==n||!1===n&&"-"!=t[4]?e.removeAttribute(t):e.setAttribute(t,"popover"==t&&1==n?"":n))}}function Te(e){return function(t){if(this.l){var n=this.l[t.type+e];if(null==t[$])t[$]=O++;else if(t[$]0?e:ce(e)?e.map(Me):de({},e)}function $e(e,t,n){try{if("function"==typeof e){var r="function"==typeof e.__u;r&&e.__u(),r&&null==t||(e.__u=e(t))}else e.current=t}catch(e){N.__e(e,n)}}function Fe(e,t,n){var r,o;if(N.unmount&&N.unmount(e),(r=e.ref)&&(r.current&&r.current!=e.__e||$e(r,null,t)),null!=(r=e.__c)){if(r.componentWillUnmount)try{r.componentWillUnmount()}catch(e){N.__e(e,t)}r.base=r.__P=null}if(r=e.__k)for(o=0;o=n.__.length&&n.__.push({}),n.__[e]}function Ge(e){return je=1,function(e,t){var n=qe(P++,2);if(n.t=e,!n.__c&&(n.__=[at(void 0,t),function(e){var t=n.__N?n.__N[0]:n.__[0],r=n.t(t,e);t!==r&&(n.__N=[r,n.__[1]],n.__c.setState({}))}],n.__c=I,!I.__f)){var r=function(e,t,r){if(!n.__c.__H)return!0;var i=n.__c.__H.__.filter(function(e){return e.__c});if(i.every(function(e){return!e.__N}))return!o||o.call(this,e,t,r);var a=n.__c.props!==e;return i.some(function(e){if(e.__N){var t=e.__[0];e.__=e.__N,e.__N=void 0,t!==e.__[0]&&(a=!0)}}),o&&o.call(this,e,t,r)||a};I.__f=!0;var o=I.shouldComponentUpdate,i=I.componentWillUpdate;I.componentWillUpdate=function(e,t,n){if(this.__e){var a=o;o=void 0,r(e,t,n),o=a}i&&i.call(this,e,t,n)},I.shouldComponentUpdate=r}return n.__N||n.__}(at,e)}function Je(e,t){var n=qe(P++,3);!Pe.__s&&it(n.__H,t)&&(n.__=e,n.u=t,I.__H.__h.push(n))}function Ye(e,t){var n=qe(P++,4);!Pe.__s&&it(n.__H,t)&&(n.__=e,n.u=t,I.__h.push(n))}function Xe(e){return je=5,Ke(function(){return{current:e}},[])}function Ke(e,t){var n=qe(P++,7);return it(n.__H,t)&&(n.__=e(),n.__H=t,n.__h=e),n.__}function Ze(e,t){return je=8,Ke(function(){return e},t)}function Qe(e){var t=I.context[e.__c],n=qe(P++,9);return n.c=e,t?(null==n.__&&(n.__=!0,t.sub(I)),t.props.value):e.__}function et(){for(var e;e=Le.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(rt),t.__h.some(ot),t.__h=[]}catch(n){t.__h=[],Pe.__e(n,e.__v)}}}Pe.__b=function(e){I=null,Ie&&Ie(e)},Pe.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),Ve&&Ve(e,t)},Pe.__r=function(e){We&&We(e),P=0;var t=(I=e.__c).__H;t&&(W===I?(t.__h=[],I.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})):(t.__h.some(rt),t.__h.some(ot),t.__h=[],P=0)),W=I},Pe.diffed=function(e){Ue&&Ue(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(1!==Le.push(t)&&U===Pe.requestAnimationFrame||((U=Pe.requestAnimationFrame)||nt)(et)),t.__H.__.some(function(e){e.u&&(e.__H=e.u),e.u=void 0})),W=I=null},Pe.__c=function(e,t){t.some(function(e){try{e.__h.some(rt),e.__h=e.__h.filter(function(e){return!e.__||ot(e)})}catch(n){t.some(function(e){e.__h&&(e.__h=[])}),t=[],Pe.__e(n,e.__v)}}),He&&He(e,t)},Pe.unmount=function(e){Be&&Be(e);var t,n=e.__c;n&&n.__H&&(n.__H.__.some(function(e){try{rt(e)}catch(e){t=e}}),n.__H=void 0,t&&Pe.__e(t,n.__v))};var tt="function"==typeof requestAnimationFrame;function nt(e){var t,n=function(){clearTimeout(r),tt&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);tt&&(t=requestAnimationFrame(n))}function rt(e){var t=I,n=e.__c;"function"==typeof n&&(e.__c=void 0,n()),I=t}function ot(e){var t=I;e.__c=e.__(),I=t}function it(e,t){return!e||e.length!==t.length||t.some(function(t,n){return t!==e[n]})}function at(e,t){return"function"==typeof t?t(e):t}var st=Symbol.for("preact-signals");function lt(){if(ft>1)ft--;else{var e,t=!1;for(!function(){var e=bt;for(bt=void 0;void 0!==e;)e.S.v===e.v&&(e.S.i=e.i),e=e.o}();void 0!==mt;){var n=mt;for(mt=void 0,gt++;void 0!==n;){var r=n.u;if(n.u=void 0,n.f&=-3,!(8&n.f)&&Nt(n))try{n.c()}catch(n){t||(e=n,t=!0)}n=r}}if(gt=0,ft--,t)throw e}}function ct(e){if(ft>0)return e();wt=++vt,ft++;try{return e()}finally{lt()}}var dt=void 0;function ut(e){var t=dt;dt=void 0;try{return e()}finally{dt=t}}var pt,ht,mt=void 0,ft=0,gt=0,vt=0,wt=0,bt=void 0,xt=0;function yt(e){if(void 0!==dt){var t=e.n;if(void 0===t||t.t!==dt)return t={i:0,S:e,p:dt.s,n:void 0,t:dt,e:void 0,x:void 0,r:t},void 0!==dt.s&&(dt.s.n=t),dt.s=t,e.n=t,32&dt.f&&e.S(t),t;if(-1===t.i)return t.i=0,void 0!==t.n&&(t.n.p=t.p,void 0!==t.p&&(t.p.n=t.n),t.p=dt.s,t.n=void 0,dt.s.n=t,dt.s=t),t}}function kt(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.l=0,this.W=null==t?void 0:t.watched,this.Z=null==t?void 0:t.unwatched,this.name=null==t?void 0:t.name}function _t(e,t){return new kt(e,t)}function Nt(e){for(var t=e.s;void 0!==t;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function St(e){for(var t=e.s;void 0!==t;t=t.n){var n=t.S.n;if(void 0!==n&&(t.r=n),t.S.n=t,t.i=-1,void 0===t.n){e.s=t;break}}}function Ct(e){for(var t=e.s,n=void 0;void 0!==t;){var r=t.p;-1===t.i?(t.S.U(t),void 0!==r&&(r.n=t.n),void 0!==t.n&&(t.n.p=r)):n=t,t.S.n=t.r,void 0!==t.r&&(t.r=void 0),t=r}e.s=n}function Tt(e,t){kt.call(this,void 0),this.x=e,this.s=void 0,this.g=xt-1,this.f=4,this.W=null==t?void 0:t.watched,this.Z=null==t?void 0:t.unwatched,this.name=null==t?void 0:t.name}function Et(e,t){return new Tt(e,t)}function zt(e){var t=e.m;if(e.m=void 0,"function"==typeof t){ft++;var n=dt;dt=void 0;try{t()}catch(t){throw e.f&=-2,e.f|=8,At(e),t}finally{dt=n,lt()}}}function At(e){for(var t=e.s;void 0!==t;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,zt(e)}function Mt(e){if(dt!==this)throw new Error("Out-of-order effect");Ct(this),dt=e,this.f&=-2,8&this.f&&At(this),lt()}function $t(e,t){this.x=e,this.m=void 0,this.s=void 0,this.u=void 0,this.f=32,this.name=null==t?void 0:t.name}function Ft(e,t){var n=new $t(e,t);try{n.c()}catch(e){throw n.d(),e}var r=n.d.bind(n);return r[Symbol.dispose]=r,r}kt.prototype.brand=st,kt.prototype.h=function(){return!0},kt.prototype.S=function(e){var t=this,n=this.t;n!==e&&void 0===e.e&&(e.x=n,this.t=e,void 0!==n?n.e=e:ut(function(){var e;null==(e=t.W)||e.call(t)}))},kt.prototype.U=function(e){var t=this;if(void 0!==this.t){var n=e.e,r=e.x;void 0!==n&&(n.x=r,e.e=void 0),void 0!==r&&(r.e=n,e.x=void 0),e===this.t&&(this.t=r,void 0===r&&ut(function(){var e;null==(e=t.Z)||e.call(t)}))}},kt.prototype.subscribe=function(e){var t=this;return Ft(function(){var n=t.value,r=dt;dt=void 0;try{e(n)}finally{dt=r}},{name:"sub"})},kt.prototype.valueOf=function(){return this.value},kt.prototype.toString=function(){return this.value+""},kt.prototype.toJSON=function(){return this.value},kt.prototype.peek=function(){var e=dt;dt=void 0;try{return this.value}finally{dt=e}},Object.defineProperty(kt.prototype,"value",{get:function(){var e=yt(this);return void 0!==e&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(gt>100)throw new Error("Cycle detected");n=this,0!==ft&&0===gt&&n.l!==wt&&(n.l=wt,bt={S:n,v:n.v,i:n.i,o:bt}),this.v=e,this.i++,xt++,ft++;try{for(var t=this.t;void 0!==t;t=t.x)t.t.N()}finally{lt()}}var n}}),Tt.prototype=new kt,Tt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===xt)return!0;if(this.g=xt,this.f|=1,this.i>0&&!Nt(this))return this.f&=-2,!0;var e=dt;try{St(this),dt=this;var t=this.x();(16&this.f||this.v!==t||0===this.i)&&(this.v=t,this.f&=-17,this.i++)}catch(e){this.v=e,this.f|=16,this.i++}return dt=e,Ct(this),this.f&=-2,!0},Tt.prototype.S=function(e){if(void 0===this.t){this.f|=36;for(var t=this.s;void 0!==t;t=t.n)t.S.S(t)}kt.prototype.S.call(this,e)},Tt.prototype.U=function(e){if(void 0!==this.t&&(kt.prototype.U.call(this,e),void 0===this.t)){this.f&=-33;for(var t=this.s;void 0!==t;t=t.n)t.S.U(t)}},Tt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;void 0!==e;e=e.x)e.t.N()}},Object.defineProperty(Tt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=yt(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),$t.prototype.c=function(){var e=this.S();try{if(8&this.f)return;if(void 0===this.x)return;var t=this.x();"function"==typeof t&&(this.m=t)}finally{e()}},$t.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,zt(this),St(this),ft++;var e=dt;return dt=this,Mt.bind(this,e)},$t.prototype.N=function(){2&this.f||(this.f|=2,this.u=mt,mt=this)},$t.prototype.d=function(){this.f|=8,1&this.f||At(this)},$t.prototype.dispose=function(){this.d()};var Rt="undefined"!=typeof window&&!!window.__PREACT_SIGNALS_DEVTOOLS__,Ot=[],Dt=[];function jt(e,t){N[e]=t.bind(null,N[e]||function(){})}function Lt(e){if(ht){var t=ht;ht=void 0,t()}ht=e&&e.S()}function Pt(e){var t=this,n=e.data,r=Wt(n);r.value=n;var o=Ke(function(){for(var e=t,n=t.__v;n=n.__;)if(n.__c){n.__c.__$f|=4;break}var o=Et(function(){var e=r.value.value;return 0===e?0:!0===e?"":e||""}),i=Et(function(){return!Array.isArray(o.value)&&!C(o.value)}),a=Ft(function(){if(this.N=Gt,i.value){var t=o.value;e.__v&&e.__v.__e&&3===e.__v.__e.nodeType&&(e.__v.__e.data=t)}}),s=t.__$u.d;return t.__$u.d=function(){a(),s.call(this)},[i,o]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}function It(e,t,n,r){var o=t in e&&void 0===e.ownerSVGElement,i=_t(n),a=n.peek();return{o:function(e,t){i.value=e,a=e.peek()},d:Ft(function(){this.N=Gt;var n=i.value.value;a!==n?(a=void 0,o?e[t]=n:null==n||!1===n&&"-"!==t[4]?e.removeAttribute(t):e.setAttribute(t,n)):a=void 0})}}function Wt(e,t){return Ke(function(){return _t(e,t)},[])}Ft(function(){pt=this.N})(),Pt.displayName="ReactiveTextNode",Object.defineProperties(kt.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Pt},props:{configurable:!0,get:function(){var e=this;return{data:{get value(){return e.value}}}}},__b:{configurable:!0,value:1}}),jt("__b",function(e,t){if("string"==typeof t.type){var n,r=t.props;for(var o in r)if("children"!==o){var i=r[o];i instanceof kt&&(n||(t.__np=n={}),n[o]=i,r[o]=i.peek())}}e(t)}),jt("__r",function(e,t){if(e(t),t.type!==me){Lt();var n,r=t.__c;r&&(r.__$f&=-2,void 0===(n=r.__$u)&&(r.__$u=(Ft(function(){o=this},{name:"function"==typeof t.type?t.type.displayName||t.type.name:""}),o.c=function(){var e;Rt&&(null==(e=n.y)||e.call(n)),r.__$f|=1,r.setState({})},n=o))),Lt(n)}var o}),jt("__e",function(e,t,n,r){Lt(),e(t,n,r)}),jt("diffed",function(e,t){var n;if(Lt(),"string"==typeof t.type&&(n=t.__e)){var r=t.__np,o=t.props;if(r){var i=n.U;if(i)for(var a in i){var s=i[a];void 0===s||a in r||(s.d(),i[a]=void 0)}else i={},n.U=i;for(var l in r){var c=i[l],d=r[l];void 0===c?(c=It(n,l,d),i[l]=c):c.o(d,o)}for(var u in r)o[u]=r[u]}}e(t)}),jt("unmount",function(e,t){if("string"==typeof t.type){var n=t.__e;if(n){var r=n.U;if(r)for(var o in n.U=void 0,r){var i=r[o];i&&i.d()}}t.__np=void 0}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)}),jt("__h",function(e,t,n,r){(r<3||9===r)&&(t.__$f|=2),e(t,n,r)}),fe.prototype.shouldComponentUpdate=function(e,t){if(this.__R)return!0;var n=this.__$u,r=n&&void 0!==n.s;for(var o in t)return!0;if(this.__f||"boolean"==typeof this.u&&!0===this.u){var i=2&this.__$f;if(!(r||i||4&this.__$f))return!0;if(1&this.__$f)return!0}else{if(!(r||4&this.__$f))return!0;if(3&this.__$f)return!0}for(var a in e)if("__source"!==a&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};var Ut="undefined"==typeof requestAnimationFrame?setTimeout:function(e){var t=function(){clearTimeout(n),cancelAnimationFrame(r),e()},n=setTimeout(t,35),r=requestAnimationFrame(t)},Ht=function(e){queueMicrotask(function(){queueMicrotask(e)})};function Bt(){ct(function(){for(var e;e=Ot.shift();)pt.call(e)})}function Vt(){1===Ot.push(this)&&(N.requestAnimationFrame||Ut)(Bt)}function qt(){ct(function(){for(var e;e=Dt.shift();)pt.call(e)})}function Gt(){1===Dt.push(this)&&(N.requestAnimationFrame||Ht)(qt)}function Jt(e,t){var n=Xe(e);n.current=e,Je(function(){return Ft(function(){return this.N=Vt,n.current()},t)},[])}function Yt(e,t){return t-e}function Xt(e){let t=e[0].name;const n=e.length,r=Math.min(4,n);for(let n=1;n{let t="";const n=new Map;for(const t of e){const{forget:e,time:r,aggregatedCount:o,name:i}=t;n.has(o)||n.set(o,[]);const a=n.get(o);a&&a.push({name:i,forget:e,time:null!=r?r:0})}const r=Array.from(n.keys()).sort(Yt),o=[];let i=0;for(const e of r){const t=n.get(e);if(!t)continue;let r=Xt(t);const a=Kt(t),s=Zt(t);i+=a,t.length>4&&(r+="…"),e>1&&(r+=` × ${e}`),s&&(r=`✨${r}`),o.push(r)}return t=o.join(", "),t.length?(t.length>40&&(t=`${t.slice(0,40)}…`),i>=.01&&(t+=` (${Number(i.toFixed(2))}ms)`),t):null};function en(e,t){return e===t||e!=e&&t!=t}var tn=()=>ie?(void 0===window.reactScanIdCounter&&(window.reactScanIdCounter=0),""+ ++window.reactScanIdCounter):"0",nn=e=>{const t=e.createOscillator(),n=e.createGain();t.connect(n),n.connect(e.destination);const r="sine",o=.3,i=.12,a=[392,600],s=o/a.length;a.forEach((n,r)=>{t.frequency.setValueAtTime(n,e.currentTime+r*s)}),t.type=r,n.gain.setValueAtTime(i,e.currentTime),n.gain.setTargetAtTime(0,e.currentTime+.7*o,.05),t.start(),t.stop(e.currentTime+o)};function rn(e,t){for(var n in t)e[n]=t[n];return e}function on(e,t){for(var n in e)if("__source"!==n&&!(n in t))return!0;for(var r in t)if("__source"!==r&&e[r]!==t[r])return!0;return!1}function an(e){try{return!((t=e.__)===(n=e.u())&&(0!==t||1/t==1/n)||t!=t&&n!=n)}catch(e){return!0}var t,n}function sn(e,t){this.props=e,this.context=t}function ln(e,t){function n(e){var t=this.props.ref;return t!=e.ref&&t&&("function"==typeof t?t(null):t.current=null),on(this.props,e)}function r(t){return this.shouldComponentUpdate=n,pe(e,t)}return r.displayName="Memo("+(e.displayName||e.name)+")",r.__f=r.prototype.isReactComponent=!0,r.type=e,r}(sn.prototype=new fe).isPureReactComponent=!0,sn.prototype.shouldComponentUpdate=function(e,t){return on(this.props,e)||on(this.state,t)};var cn=N.__b;N.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),cn&&cn(e)};var dn="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.forward_ref")||3911;function un(e){function t(t){var n=rn({},t);return delete n.ref,e(n,t.ref||null)}return t.$$typeof=dn,t.render=e,t.prototype.isReactComponent=t.__f=!0,t.displayName="ForwardRef("+(e.displayName||e.name)+")",t}var pn=N.__e;N.__e=function(e,t,n,r){if(e.then)for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return null==t.__e&&(t.__e=n.__e,t.__k=n.__k),o.__c(e,t);pn(e,t,n,r)};var hn=N.unmount;function mn(e,t,n){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(e){"function"==typeof e.__c&&e.__c()}),e.__c.__H=null),null!=(e=rn({},e)).__c&&(e.__c.__P===n&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(e){return mn(e,t,n)})),e}function fn(e,t,n){return e&&n&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(e){return fn(e,t,n)}),e.__c&&e.__c.__P===t&&(e.__e&&n.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=n)),e}function gn(){this.__u=0,this.o=null,this.__b=null}function vn(e){var t=e.__&&e.__.__c;return t&&t.__a&&t.__a(e)}function wn(){this.i=null,this.l=null}N.unmount=function(e){var t=e.__c;t&&(t.__z=!0),t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),hn&&hn(e)},(gn.prototype=new fe).__c=function(e,t){var n=t.__c,r=this;null==r.o&&(r.o=[]),r.o.push(n);var o=vn(r.__v),i=!1,a=function(){i||r.__z||(i=!0,n.__R=null,o?o(l):l())};n.__R=a;var s=n.__P;n.__P=null;var l=function(){if(! --r.__u){if(r.state.__a){var e=r.state.__a;r.__v.__k[0]=fn(e,e.__c.__P,e.__c.__O)}var t;for(r.setState({__a:r.__b=null});t=r.o.pop();)t.__P=s,t.forceUpdate()}};r.__u++||32&t.__u||r.setState({__a:r.__b=r.__v.__k[0]}),e.then(a,a)},gn.prototype.componentWillUnmount=function(){this.o=[]},gn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var n=document.createElement("div"),r=this.__v.__k[0].__c;this.__v.__k[0]=mn(this.__b,n,r.__O=r.__P)}this.__b=null}var o=t.__a&&pe(me,null,e.fallback);return o&&(o.__u&=-33),[pe(me,null,t.__a?null:e.children),o]};var bn=function(e,t,n){if(++n[1]===n[0]&&e.l.delete(t),e.props.revealOrder&&("t"!==e.props.revealOrder[0]||!e.l.size))for(n=e.i;n;){for(;n.length>3;)n.pop()();if(n[1]>>1,1),t.h.removeChild(e)}}}Oe(pe(xn,{context:t.context},e.__v),t.v)}(wn.prototype=new fe).__a=function(e){var t=this,n=vn(t.__v),r=t.l.get(e);return r[0]++,function(o){var i=function(){t.props.revealOrder?(r.push(o),bn(t,e,r)):o()};n?n(i):i()}},wn.prototype.render=function(e){this.i=null,this.l=new Map;var t=_e(e.children);e.revealOrder&&"b"===e.revealOrder[0]&&t.reverse();for(var n=t.length;n--;)this.l.set(t[n],this.i=[1,0,this.i]);return e.children},wn.prototype.componentDidUpdate=wn.prototype.componentDidMount=function(){var e=this;this.l.forEach(function(t,n){bn(e,n,t)})};var kn="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,_n=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,Nn=/^on(Ani|Tra|Tou|BeforeInp|Compo)/,Sn=/[A-Z0-9]/g,Cn="undefined"!=typeof document,Tn=function(e){return("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/:/fil|che|ra/).test(e)};fe.prototype.isReactComponent=!0,["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(e){Object.defineProperty(fe.prototype,e,{configurable:!0,get:function(){return this["UNSAFE_"+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var En=N.event;N.event=function(e){return En&&(e=En(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var zn={configurable:!0,get:function(){return this.class}},An=N.vnode;N.vnode=function(e){"string"==typeof e.type&&function(e){var t=e.props,n=e.type,r={},o=-1==n.indexOf("-");for(var i in t){var a=t[i];if(!("value"===i&&"defaultValue"in t&&null==a||Cn&&"children"===i&&"noscript"===n||"class"===i||"className"===i)){var s=i.toLowerCase();"defaultValue"===i&&"value"in t&&null==t.value?i="value":"download"===i&&!0===a?a="":"translate"===s&&"no"===a?a=!1:"o"===s[0]&&"n"===s[1]?"ondoubleclick"===s?i="ondblclick":"onchange"!==s||"input"!==n&&"textarea"!==n||Tn(t.type)?"onfocus"===s?i="onfocusin":"onblur"===s?i="onfocusout":Nn.test(i)&&(i=s):s=i="oninput":o&&_n.test(i)?i=i.replace(Sn,"-$&").toLowerCase():null===a&&(a=void 0),"oninput"===s&&r[i=s]&&(i="oninputCapture"),r[i]=a}}"select"==n&&(r.multiple&&Array.isArray(r.value)&&(r.value=_e(t.children).forEach(function(e){e.props.selected=-1!=r.value.indexOf(e.props.value)})),null!=r.defaultValue&&(r.value=_e(t.children).forEach(function(e){e.props.selected=r.multiple?-1!=r.defaultValue.indexOf(e.props.value):r.defaultValue==e.props.value}))),t.class&&!t.className?(r.class=t.class,Object.defineProperty(r,"className",zn)):t.className&&(r.class=r.className=t.className),e.props=r}(e),e.$$typeof=kn,An&&An(e)};var Mn=N.__r;N.__r=function(e){Mn&&Mn(e),e.__c};var $n=N.diffed;N.diffed=function(e){$n&&$n(e);var t=e.props,n=e.__e;null!=n&&"textarea"===e.type&&"value"in t&&t.value!==n.value&&(n.value=null==t.value?"":t.value)};var Fn=0;function Rn(e,t,n,r,o,i){t||(t={});var a,s,l=t;if("ref"in l)for(s in l={},t)"ref"==s?a=t[s]:l[s]=t[s];var c={type:e,props:l,key:n,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--Fn,__i:-1,__u:0,__source:o,__self:i};if("function"==typeof e&&(a=e.defaultProps))for(s in a)void 0===l[s]&&(l[s]=a[s]);return N.vnode&&N.vnode(c),c}var On=un(({size:e=15,name:t,fill:n="currentColor",stroke:r="currentColor",className:o,externalURL:i="",style:a},s)=>{const l=Array.isArray(e)?e[0]:e,c=Array.isArray(e)?e[1]||e[0]:e,d=`${i}#${t}`;return Rn("svg",{ref:s,width:`${l}px`,height:`${c}px`,fill:n,stroke:r,className:o,style:{...a,minWidth:`${l}px`,maxWidth:`${l}px`,minHeight:`${c}px`,maxHeight:`${c}px`},children:[Rn("title",{children:t}),Rn("use",{href:d})]})}),Dn=24,jn=550,Ln=350,Pn=400,In=240,Wn="react-scan-widget-settings-v2",Un="react-scan-widget-collapsed-v1",Hn="react-scan-widget-last-view-v1";function Bn(e){var t,n,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t({nextPart:e,validators:t,classGroupId:n}),qn="-",Gn=[],Jn=e=>{const t=Kn(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:e=>{if(e.startsWith("[")&&e.endsWith("]"))return Xn(e);const n=e.split(qn),r=""===n[0]&&n.length>1?1:0;return Yn(n,r,t)},getConflictingClassGroupIds:(e,t)=>{if(t){const t=r[e],o=n[e];return t?o?((e,t)=>{const n=new Array(e.length+t.length);for(let t=0;t{if(0===e.length-t)return n.classGroupId;const r=e[t],o=n.nextPart.get(r);if(o){const n=Yn(e,t+1,o);if(n)return n}const i=n.validators;if(null===i)return;const a=0===t?e.join(qn):e.slice(t).join(qn),s=i.length;for(let e=0;e-1===e.slice(1,-1).indexOf(":")?void 0:(()=>{const t=e.slice(1,-1),n=t.indexOf(":"),r=t.slice(0,n);return r?"arbitrary.."+r:void 0})(),Kn=e=>{const{theme:t,classGroups:n}=e;return Zn(n,t)},Zn=(e,t)=>{const n=Vn();for(const r in e){const o=e[r];Qn(o,n,r,t)}return n},Qn=(e,t,n,r)=>{const o=e.length;for(let i=0;i{"string"!=typeof e?"function"!=typeof e?rr(e,t,n,r):nr(e,t,n,r):tr(e,t,n)},tr=(e,t,n)=>{(""===e?t:or(t,e)).classGroupId=n},nr=(e,t,n,r)=>{ir(e)?Qn(e(r),t,n,r):(null===t.validators&&(t.validators=[]),t.validators.push(((e,t)=>({classGroupId:e,validator:t}))(n,e)))},rr=(e,t,n,r)=>{const o=Object.entries(e),i=o.length;for(let e=0;e{let n=e;const r=t.split(qn),o=r.length;for(let e=0;e"isThemeGetter"in e&&!0===e.isThemeGetter,ar=e=>{if(e<1)return{get:()=>{},set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null);const o=(o,i)=>{n[o]=i,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];return void 0!==t?t:void 0!==(t=r[e])?(o(e,t),t):void 0},set(e,t){e in n?n[e]=t:o(e,t)}}},sr=[],lr=(e,t,n,r,o)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:o}),cr=e=>{const{prefix:t,experimentalParseClassName:n}=e;let r=e=>{const t=[];let n,r=0,o=0,i=0;const a=e.length;for(let s=0;si?n-i:void 0)};if(t){const e=t+":",n=r;r=t=>t.startsWith(e)?n(t.slice(e.length)):lr(sr,!1,t,void 0,!0)}if(n){const e=r;r=t=>n({className:t,parseClassName:e})}return r},dr=e=>{const t=new Map;return e.orderSensitiveModifiers.forEach((e,n)=>{t.set(e,1e6+n)}),e=>{const n=[];let r=[];for(let o=0;o0&&(r.sort(),n.push(...r),r=[]),n.push(i)):r.push(i)}return r.length>0&&(r.sort(),n.push(...r)),n}},ur=/\s+/,pr=e=>{if("string"==typeof e)return e;let t,n="";for(let r=0;r{const t=t=>t[e]||hr;return t.isThemeGetter=!0,t},fr=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,gr=/^\((?:(\w[\w-]*):)?(.+)\)$/i,vr=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,wr=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,br=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,xr=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,yr=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,kr=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,_r=e=>vr.test(e),Nr=e=>!!e&&!Number.isNaN(Number(e)),Sr=e=>!!e&&Number.isInteger(Number(e)),Cr=e=>e.endsWith("%")&&Nr(e.slice(0,-1)),Tr=e=>wr.test(e),Er=()=>!0,zr=e=>br.test(e)&&!xr.test(e),Ar=()=>!1,Mr=e=>yr.test(e),$r=e=>kr.test(e),Fr=e=>!Or(e)&&!Hr(e),Rr=e=>Kr(e,to,Ar),Or=e=>fr.test(e),Dr=e=>Kr(e,no,zr),jr=e=>Kr(e,ro,Nr),Lr=e=>Kr(e,io,Er),Pr=e=>Kr(e,oo,Ar),Ir=e=>Kr(e,Qr,Ar),Wr=e=>Kr(e,eo,$r),Ur=e=>Kr(e,ao,Mr),Hr=e=>gr.test(e),Br=e=>Zr(e,no),Vr=e=>Zr(e,oo),qr=e=>Zr(e,Qr),Gr=e=>Zr(e,to),Jr=e=>Zr(e,eo),Yr=e=>Zr(e,ao,!0),Xr=e=>Zr(e,io,!0),Kr=(e,t,n)=>{const r=fr.exec(e);return!!r&&(r[1]?t(r[1]):n(r[2]))},Zr=(e,t,n=!1)=>{const r=gr.exec(e);return!!r&&(r[1]?t(r[1]):n)},Qr=e=>"position"===e||"percentage"===e,eo=e=>"image"===e||"url"===e,to=e=>"length"===e||"size"===e||"bg-size"===e,no=e=>"length"===e,ro=e=>"number"===e,oo=e=>"family-name"===e,io=e=>"number"===e||"weight"===e,ao=e=>"shadow"===e,so=((e,...t)=>{let n,r,o,i;const a=e=>{const t=r(e);if(t)return t;const i=((e,t)=>{const{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:o,sortModifiers:i}=t,a=[],s=e.trim().split(ur);let l="";for(let e=s.length-1;e>=0;e-=1){const t=s[e],{isExternal:c,modifiers:d,hasImportantModifier:u,baseClassName:p,maybePostfixModifierPosition:h}=n(t);if(c){l=t+(l.length>0?" "+l:l);continue}let m=!!h,f=r(m?p.substring(0,h):p);if(!f){if(!m){l=t+(l.length>0?" "+l:l);continue}if(f=r(p),!f){l=t+(l.length>0?" "+l:l);continue}m=!1}const g=0===d.length?"":1===d.length?d[0]:i(d).join(":"),v=u?g+"!":g,w=v+f;if(a.indexOf(w)>-1)continue;a.push(w);const b=o(f,m);for(let e=0;e0?" "+l:l)}return l})(e,n);return o(e,i),i};return i=s=>{const l=t.reduce((e,t)=>t(e),e());return n=(e=>({cache:ar(e.cacheSize),parseClassName:cr(e),sortModifiers:dr(e),...Jn(e)}))(l),r=n.cache.get,o=n.cache.set,i=a,a(s)},(...e)=>i(((...e)=>{let t,n,r=0,o="";for(;r{const e=mr("color"),t=mr("font"),n=mr("text"),r=mr("font-weight"),o=mr("tracking"),i=mr("leading"),a=mr("breakpoint"),s=mr("container"),l=mr("spacing"),c=mr("radius"),d=mr("shadow"),u=mr("inset-shadow"),p=mr("text-shadow"),h=mr("drop-shadow"),m=mr("blur"),f=mr("perspective"),g=mr("aspect"),v=mr("ease"),w=mr("animate"),b=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom",Hr,Or],x=()=>[Hr,Or,l],y=()=>[_r,"full","auto",...x()],k=()=>[Sr,"none","subgrid",Hr,Or],_=()=>["auto",{span:["full",Sr,Hr,Or]},Sr,Hr,Or],N=()=>[Sr,"auto",Hr,Or],S=()=>["auto","min","max","fr",Hr,Or],C=()=>["auto",...x()],T=()=>[_r,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...x()],E=()=>[_r,"screen","full","dvw","lvw","svw","min","max","fit",...x()],z=()=>[_r,"screen","full","lh","dvh","lvh","svh","min","max","fit",...x()],A=()=>[e,Hr,Or],M=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom",qr,Ir,{position:[Hr,Or]}],$=()=>["auto","cover","contain",Gr,Rr,{size:[Hr,Or]}],F=()=>[Cr,Br,Dr],R=()=>["","none","full",c,Hr,Or],O=()=>["",Nr,Br,Dr],D=()=>[Nr,Cr,qr,Ir],j=()=>["","none",m,Hr,Or],L=()=>["none",Nr,Hr,Or],P=()=>["none",Nr,Hr,Or],I=()=>[Nr,Hr,Or],W=()=>[_r,"full",...x()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[Tr],breakpoint:[Tr],color:[Er],container:[Tr],"drop-shadow":[Tr],ease:["in","out","in-out"],font:[Fr],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[Tr],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[Tr],shadow:[Tr],spacing:["px",Nr],text:[Tr],"text-shadow":[Tr],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",_r,Or,Hr,g]}],container:["container"],columns:[{columns:[Nr,Or,Hr,s]}],"break-after":[{"break-after":["auto","avoid","all","avoid-page","page","left","right","column"]}],"break-before":[{"break-before":["auto","avoid","all","avoid-page","page","left","right","column"]}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:b()}],overflow:[{overflow:["auto","hidden","clip","visible","scroll"]}],"overflow-x":[{"overflow-x":["auto","hidden","clip","visible","scroll"]}],"overflow-y":[{"overflow-y":["auto","hidden","clip","visible","scroll"]}],overscroll:[{overscroll:["auto","contain","none"]}],"overscroll-x":[{"overscroll-x":["auto","contain","none"]}],"overscroll-y":[{"overscroll-y":["auto","contain","none"]}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:y()}],"inset-x":[{"inset-x":y()}],"inset-y":[{"inset-y":y()}],start:[{"inset-s":y(),start:y()}],end:[{"inset-e":y(),end:y()}],"inset-bs":[{"inset-bs":y()}],"inset-be":[{"inset-be":y()}],top:[{top:y()}],right:[{right:y()}],bottom:[{bottom:y()}],left:[{left:y()}],visibility:["visible","invisible","collapse"],z:[{z:[Sr,"auto",Hr,Or]}],basis:[{basis:[_r,"full","auto",s,...x()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[Nr,_r,"auto","initial","none",Or]}],grow:[{grow:["",Nr,Hr,Or]}],shrink:[{shrink:["",Nr,Hr,Or]}],order:[{order:[Sr,"first","last","none",Hr,Or]}],"grid-cols":[{"grid-cols":k()}],"col-start-end":[{col:_()}],"col-start":[{"col-start":N()}],"col-end":[{"col-end":N()}],"grid-rows":[{"grid-rows":k()}],"row-start-end":[{row:_()}],"row-start":[{"row-start":N()}],"row-end":[{"row-end":N()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":S()}],"auto-rows":[{"auto-rows":S()}],gap:[{gap:x()}],"gap-x":[{"gap-x":x()}],"gap-y":[{"gap-y":x()}],"justify-content":[{justify:["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe","normal"]}],"justify-items":[{"justify-items":["start","end","center","stretch","center-safe","end-safe","normal"]}],"justify-self":[{"justify-self":["auto","start","end","center","stretch","center-safe","end-safe"]}],"align-content":[{content:["normal","start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"]}],"align-items":[{items:["start","end","center","stretch","center-safe","end-safe",{baseline:["","last"]}]}],"align-self":[{self:["auto","start","end","center","stretch","center-safe","end-safe",{baseline:["","last"]}]}],"place-content":[{"place-content":["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"]}],"place-items":[{"place-items":["start","end","center","stretch","center-safe","end-safe","baseline"]}],"place-self":[{"place-self":["auto","start","end","center","stretch","center-safe","end-safe"]}],p:[{p:x()}],px:[{px:x()}],py:[{py:x()}],ps:[{ps:x()}],pe:[{pe:x()}],pbs:[{pbs:x()}],pbe:[{pbe:x()}],pt:[{pt:x()}],pr:[{pr:x()}],pb:[{pb:x()}],pl:[{pl:x()}],m:[{m:C()}],mx:[{mx:C()}],my:[{my:C()}],ms:[{ms:C()}],me:[{me:C()}],mbs:[{mbs:C()}],mbe:[{mbe:C()}],mt:[{mt:C()}],mr:[{mr:C()}],mb:[{mb:C()}],ml:[{ml:C()}],"space-x":[{"space-x":x()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":x()}],"space-y-reverse":["space-y-reverse"],size:[{size:T()}],"inline-size":[{inline:["auto",...E()]}],"min-inline-size":[{"min-inline":["auto",...E()]}],"max-inline-size":[{"max-inline":["none",...E()]}],"block-size":[{block:["auto",...z()]}],"min-block-size":[{"min-block":["auto",...z()]}],"max-block-size":[{"max-block":["none",...z()]}],w:[{w:[s,"screen",...T()]}],"min-w":[{"min-w":[s,"screen","none",...T()]}],"max-w":[{"max-w":[s,"screen","none","prose",{screen:[a]},...T()]}],h:[{h:["screen","lh",...T()]}],"min-h":[{"min-h":["screen","lh","none",...T()]}],"max-h":[{"max-h":["screen","lh",...T()]}],"font-size":[{text:["base",n,Br,Dr]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[r,Xr,Lr]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",Cr,Or]}],"font-family":[{font:[Vr,Pr,t]}],"font-features":[{"font-features":[Or]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[o,Hr,Or]}],"line-clamp":[{"line-clamp":[Nr,"none",Hr,jr]}],leading:[{leading:[i,...x()]}],"list-image":[{"list-image":["none",Hr,Or]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",Hr,Or]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:A()}],"text-color":[{text:A()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:["solid","dashed","dotted","double","wavy"]}],"text-decoration-thickness":[{decoration:[Nr,"from-font","auto",Hr,Dr]}],"text-decoration-color":[{decoration:A()}],"underline-offset":[{"underline-offset":[Nr,"auto",Hr,Or]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:x()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",Hr,Or]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",Hr,Or]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:M()}],"bg-repeat":[{bg:["no-repeat",{repeat:["","x","y","space","round"]}]}],"bg-size":[{bg:$()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},Sr,Hr,Or],radial:["",Hr,Or],conic:[Sr,Hr,Or]},Jr,Wr]}],"bg-color":[{bg:A()}],"gradient-from-pos":[{from:F()}],"gradient-via-pos":[{via:F()}],"gradient-to-pos":[{to:F()}],"gradient-from":[{from:A()}],"gradient-via":[{via:A()}],"gradient-to":[{to:A()}],rounded:[{rounded:R()}],"rounded-s":[{"rounded-s":R()}],"rounded-e":[{"rounded-e":R()}],"rounded-t":[{"rounded-t":R()}],"rounded-r":[{"rounded-r":R()}],"rounded-b":[{"rounded-b":R()}],"rounded-l":[{"rounded-l":R()}],"rounded-ss":[{"rounded-ss":R()}],"rounded-se":[{"rounded-se":R()}],"rounded-ee":[{"rounded-ee":R()}],"rounded-es":[{"rounded-es":R()}],"rounded-tl":[{"rounded-tl":R()}],"rounded-tr":[{"rounded-tr":R()}],"rounded-br":[{"rounded-br":R()}],"rounded-bl":[{"rounded-bl":R()}],"border-w":[{border:O()}],"border-w-x":[{"border-x":O()}],"border-w-y":[{"border-y":O()}],"border-w-s":[{"border-s":O()}],"border-w-e":[{"border-e":O()}],"border-w-bs":[{"border-bs":O()}],"border-w-be":[{"border-be":O()}],"border-w-t":[{"border-t":O()}],"border-w-r":[{"border-r":O()}],"border-w-b":[{"border-b":O()}],"border-w-l":[{"border-l":O()}],"divide-x":[{"divide-x":O()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":O()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:["solid","dashed","dotted","double","hidden","none"]}],"divide-style":[{divide:["solid","dashed","dotted","double","hidden","none"]}],"border-color":[{border:A()}],"border-color-x":[{"border-x":A()}],"border-color-y":[{"border-y":A()}],"border-color-s":[{"border-s":A()}],"border-color-e":[{"border-e":A()}],"border-color-bs":[{"border-bs":A()}],"border-color-be":[{"border-be":A()}],"border-color-t":[{"border-t":A()}],"border-color-r":[{"border-r":A()}],"border-color-b":[{"border-b":A()}],"border-color-l":[{"border-l":A()}],"divide-color":[{divide:A()}],"outline-style":[{outline:["solid","dashed","dotted","double","none","hidden"]}],"outline-offset":[{"outline-offset":[Nr,Hr,Or]}],"outline-w":[{outline:["",Nr,Br,Dr]}],"outline-color":[{outline:A()}],shadow:[{shadow:["","none",d,Yr,Ur]}],"shadow-color":[{shadow:A()}],"inset-shadow":[{"inset-shadow":["none",u,Yr,Ur]}],"inset-shadow-color":[{"inset-shadow":A()}],"ring-w":[{ring:O()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:A()}],"ring-offset-w":[{"ring-offset":[Nr,Dr]}],"ring-offset-color":[{"ring-offset":A()}],"inset-ring-w":[{"inset-ring":O()}],"inset-ring-color":[{"inset-ring":A()}],"text-shadow":[{"text-shadow":["none",p,Yr,Ur]}],"text-shadow-color":[{"text-shadow":A()}],opacity:[{opacity:[Nr,Hr,Or]}],"mix-blend":[{"mix-blend":["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity","plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"]}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[Nr]}],"mask-image-linear-from-pos":[{"mask-linear-from":D()}],"mask-image-linear-to-pos":[{"mask-linear-to":D()}],"mask-image-linear-from-color":[{"mask-linear-from":A()}],"mask-image-linear-to-color":[{"mask-linear-to":A()}],"mask-image-t-from-pos":[{"mask-t-from":D()}],"mask-image-t-to-pos":[{"mask-t-to":D()}],"mask-image-t-from-color":[{"mask-t-from":A()}],"mask-image-t-to-color":[{"mask-t-to":A()}],"mask-image-r-from-pos":[{"mask-r-from":D()}],"mask-image-r-to-pos":[{"mask-r-to":D()}],"mask-image-r-from-color":[{"mask-r-from":A()}],"mask-image-r-to-color":[{"mask-r-to":A()}],"mask-image-b-from-pos":[{"mask-b-from":D()}],"mask-image-b-to-pos":[{"mask-b-to":D()}],"mask-image-b-from-color":[{"mask-b-from":A()}],"mask-image-b-to-color":[{"mask-b-to":A()}],"mask-image-l-from-pos":[{"mask-l-from":D()}],"mask-image-l-to-pos":[{"mask-l-to":D()}],"mask-image-l-from-color":[{"mask-l-from":A()}],"mask-image-l-to-color":[{"mask-l-to":A()}],"mask-image-x-from-pos":[{"mask-x-from":D()}],"mask-image-x-to-pos":[{"mask-x-to":D()}],"mask-image-x-from-color":[{"mask-x-from":A()}],"mask-image-x-to-color":[{"mask-x-to":A()}],"mask-image-y-from-pos":[{"mask-y-from":D()}],"mask-image-y-to-pos":[{"mask-y-to":D()}],"mask-image-y-from-color":[{"mask-y-from":A()}],"mask-image-y-to-color":[{"mask-y-to":A()}],"mask-image-radial":[{"mask-radial":[Hr,Or]}],"mask-image-radial-from-pos":[{"mask-radial-from":D()}],"mask-image-radial-to-pos":[{"mask-radial-to":D()}],"mask-image-radial-from-color":[{"mask-radial-from":A()}],"mask-image-radial-to-color":[{"mask-radial-to":A()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"]}],"mask-image-conic-pos":[{"mask-conic":[Nr]}],"mask-image-conic-from-pos":[{"mask-conic-from":D()}],"mask-image-conic-to-pos":[{"mask-conic-to":D()}],"mask-image-conic-from-color":[{"mask-conic-from":A()}],"mask-image-conic-to-color":[{"mask-conic-to":A()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:M()}],"mask-repeat":[{mask:["no-repeat",{repeat:["","x","y","space","round"]}]}],"mask-size":[{mask:$()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",Hr,Or]}],filter:[{filter:["","none",Hr,Or]}],blur:[{blur:j()}],brightness:[{brightness:[Nr,Hr,Or]}],contrast:[{contrast:[Nr,Hr,Or]}],"drop-shadow":[{"drop-shadow":["","none",h,Yr,Ur]}],"drop-shadow-color":[{"drop-shadow":A()}],grayscale:[{grayscale:["",Nr,Hr,Or]}],"hue-rotate":[{"hue-rotate":[Nr,Hr,Or]}],invert:[{invert:["",Nr,Hr,Or]}],saturate:[{saturate:[Nr,Hr,Or]}],sepia:[{sepia:["",Nr,Hr,Or]}],"backdrop-filter":[{"backdrop-filter":["","none",Hr,Or]}],"backdrop-blur":[{"backdrop-blur":j()}],"backdrop-brightness":[{"backdrop-brightness":[Nr,Hr,Or]}],"backdrop-contrast":[{"backdrop-contrast":[Nr,Hr,Or]}],"backdrop-grayscale":[{"backdrop-grayscale":["",Nr,Hr,Or]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[Nr,Hr,Or]}],"backdrop-invert":[{"backdrop-invert":["",Nr,Hr,Or]}],"backdrop-opacity":[{"backdrop-opacity":[Nr,Hr,Or]}],"backdrop-saturate":[{"backdrop-saturate":[Nr,Hr,Or]}],"backdrop-sepia":[{"backdrop-sepia":["",Nr,Hr,Or]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":x()}],"border-spacing-x":[{"border-spacing-x":x()}],"border-spacing-y":[{"border-spacing-y":x()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",Hr,Or]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[Nr,"initial",Hr,Or]}],ease:[{ease:["linear","initial",v,Hr,Or]}],delay:[{delay:[Nr,Hr,Or]}],animate:[{animate:["none",w,Hr,Or]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[f,Hr,Or]}],"perspective-origin":[{"perspective-origin":b()}],rotate:[{rotate:L()}],"rotate-x":[{"rotate-x":L()}],"rotate-y":[{"rotate-y":L()}],"rotate-z":[{"rotate-z":L()}],scale:[{scale:P()}],"scale-x":[{"scale-x":P()}],"scale-y":[{"scale-y":P()}],"scale-z":[{"scale-z":P()}],"scale-3d":["scale-3d"],skew:[{skew:I()}],"skew-x":[{"skew-x":I()}],"skew-y":[{"skew-y":I()}],transform:[{transform:[Hr,Or,"","none","gpu","cpu"]}],"transform-origin":[{origin:b()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:W()}],"translate-x":[{"translate-x":W()}],"translate-y":[{"translate-y":W()}],"translate-z":[{"translate-z":W()}],"translate-none":["translate-none"],accent:[{accent:A()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:A()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",Hr,Or]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":x()}],"scroll-mx":[{"scroll-mx":x()}],"scroll-my":[{"scroll-my":x()}],"scroll-ms":[{"scroll-ms":x()}],"scroll-me":[{"scroll-me":x()}],"scroll-mbs":[{"scroll-mbs":x()}],"scroll-mbe":[{"scroll-mbe":x()}],"scroll-mt":[{"scroll-mt":x()}],"scroll-mr":[{"scroll-mr":x()}],"scroll-mb":[{"scroll-mb":x()}],"scroll-ml":[{"scroll-ml":x()}],"scroll-p":[{"scroll-p":x()}],"scroll-px":[{"scroll-px":x()}],"scroll-py":[{"scroll-py":x()}],"scroll-ps":[{"scroll-ps":x()}],"scroll-pe":[{"scroll-pe":x()}],"scroll-pbs":[{"scroll-pbs":x()}],"scroll-pbe":[{"scroll-pbe":x()}],"scroll-pt":[{"scroll-pt":x()}],"scroll-pr":[{"scroll-pr":x()}],"scroll-pb":[{"scroll-pb":x()}],"scroll-pl":[{"scroll-pl":x()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",Hr,Or]}],fill:[{fill:["none",...A()]}],"stroke-w":[{stroke:[Nr,Br,Dr,jr]}],stroke:[{stroke:["none",...A()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","inset-bs","inset-be","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pbs","pbe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mbs","mbe","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-bs","border-w-be","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-bs","border-color-be","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mbs","scroll-mbe","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pbs","scroll-pbe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}}),lo=(...e)=>so(function(){for(var e,t,n=0,r="",o=arguments.length;n{let n=0;return r=>{const o=Date.now();if(o-n>=t)return n=o,e(r)}},uo=e=>{if(!ie)return null;try{const t=localStorage.getItem(e);return t?JSON.parse(t):null}catch{return null}},po=(e,t)=>{if(ie)try{window.localStorage.setItem(e,JSON.stringify(t))}catch{}},ho=e=>{if(ie)try{window.localStorage.removeItem(e)}catch{}},mo=e=>{if(!e)return{name:"Unknown",wrappers:[],wrapperTypes:[]};const{tag:t,type:n,elementType:r}=e;let o=J(n);const i=[],a=[];if(q(e)||15===t||14===t||(null==n?void 0:n.$$typeof)===Symbol.for("react.memo")||(null==r?void 0:r.$$typeof)===Symbol.for("react.memo")){const t=q(e);a.push({type:"memo",title:t?"This component has been auto-memoized by the React Compiler.":"Memoized component that skips re-renders if props are the same",compiler:t})}if(24===t&&a.push({type:"lazy",title:"Lazily loaded component that supports code splitting"}),13===t&&a.push({type:"suspense",title:"Component that can suspend while content is loading"}),12===t&&a.push({type:"profiler",title:"Component that measures rendering performance"}),"string"==typeof o){const e=/^(\w+)\((.*)\)$/;let t=o;for(;e.test(t);){const n=t.match(e);if(!(null==n?void 0:n[1])||!(null==n?void 0:n[2]))break;i.unshift(n[1]),t=n[2]}o=t}return{name:o||"Unknown",wrappers:i,wrapperTypes:a}},fo=e=>"number"==typeof e&&Number.isFinite(e)&&e>=0,go=e=>Boolean(e)&&"object"==typeof e&&!Array.isArray(e),vo=()=>{const e=Pu.options.value.safeArea;if(fo(e))return{top:e,right:e,bottom:e,left:e};if(go(e)){const t=e.top,n=e.right,r=e.bottom,o=e.left;return{top:fo(t)?t:Dn,right:fo(n)?n:Dn,bottom:fo(r)?r:Dn,left:fo(o)?o:Dn}}return{top:Dn,right:Dn,bottom:Dn,left:Dn}},wo=_t(!1),bo=_t(null),xo=()=>({corner:"bottom-right",dimensions:{isFullWidth:!1,isFullHeight:!1,width:jn,height:Ln,position:{x:Dn,y:Dn}},lastDimensions:{isFullWidth:!1,isFullHeight:!1,width:jn,height:Ln,position:{x:Dn,y:Dn}},componentsTree:{width:In}}),yo=_t((()=>{var e,t,n,r,o;const i=xo(),a=uo(Wn);return a?{corner:null!=(e=a.corner)?e:i.corner,dimensions:null!=(t=a.dimensions)?t:i.dimensions,lastDimensions:null!=(r=null!=(n=a.lastDimensions)?n:a.dimensions)?r:i.lastDimensions,componentsTree:null!=(o=a.componentsTree)?o:i.componentsTree}:(po(Wn,{corner:i.corner,dimensions:i.dimensions,lastDimensions:i.lastDimensions,componentsTree:i.componentsTree}),i)})()),ko=()=>{if(!ie)return;const{dimensions:e}=yo.value,{width:t,height:n,position:r}=e,o=vo();yo.value={...yo.value,dimensions:{isFullWidth:t>=window.innerWidth-o.left-o.right,isFullHeight:n>=window.innerHeight-o.top-o.bottom,width:t,height:n,position:r}}},_o=_t({view:"none"}),No=uo(Un),So=_t(null!=No?No:null);function Co(){return!1}function To(e){function t(t){return this.shouldComponentUpdate=Co,pe(e,t)}return t.displayName=`Memo(${e.displayName||e.name})`,t.prototype.isReactComponent=!0,t._forwarded=!0,t}var Eo={updates:[],currentFiber:null,totalUpdates:0,windowOffset:0,currentIndex:0,isViewingHistory:!1,latestFiber:null,isVisible:!1,playbackSpeed:1},zo=_t(Eo),Ao=_t(0),Mo=[],$o=null,Fo=(e,t)=>{if(Mo.push({update:e,fiber:t}),!$o){const e=()=>{(()=>{if(0===Mo.length)return;const e=[...Mo],{updates:t,totalUpdates:n,currentIndex:r,isViewingHistory:o}=zo.value,i=[...t];let a=n;for(const{update:t}of e)i.length>=1e3&&i.shift(),i.push(t),a++;const s=Math.max(0,a-1e3);let l;l=o?r===n-1?i.length-1:0===r?0:0===s?r:r-1:i.length-1;const c=e[e.length-1];zo.value={...zo.value,latestFiber:c.fiber,updates:i,totalUpdates:a,windowOffset:s,currentIndex:l,isViewingHistory:o},Mo=Mo.slice(e.length)})(),$o=null,Mo.length>0&&($o=setTimeout(e,96))};$o=setTimeout(e,96)}},Ro=()=>{$o&&(clearTimeout($o),$o=null),Mo=[],zo.value=Eo},Oo=_t({query:"",matches:[],currentMatchIndex:-1}),Do=_t(!1),jo=(e,t=0,n=null)=>e.reduce((e,r,o)=>{var i,a;const s=r.element?(e=>{var t;const n=[];let r=e;for(;r;){const e=r.elementType,o="function"==typeof e?e.displayName||e.name:"string"==typeof e?e:"Unknown",i=void 0!==r.index?`[${r.index}]`:"";n.unshift(`${o}${i}`),r=null!=(t=r.return)?t:null}return n.join("::")})(r.fiber):`${n}-${o}`,l=(null==(i=r.fiber)?void 0:i.type)?ma(r.fiber):void 0,c={...r,depth:t,nodeId:s,parentId:n,fiber:r.fiber,renderData:l};return e.push(c),(null==(a=r.children)?void 0:a.length)&&e.push(...jo(r.children,t+1,s)),e},[]),Lo=["memo","forwardRef","lazy","suspense"],Po=e=>{const t=e.match(/\[(.*?)\]/);if(!t)return null;const n=[],r=t[1].split(",");for(const e of r){const t=e.trim().toLowerCase();t&&n.push(t)}return n},Io=(e,t)=>{if(0===e.length)return!0;if(!t.length)return!1;for(const n of e){let e=!1;for(const r of t)if(r.type.toLowerCase().includes(n)){e=!0;break}if(!e)return!1}return!0},Wo=e=>e>0?e<.1-Number.EPSILON?"< 0.1":e<1e3?Number(e.toFixed(1)).toString():`${(e/1e3).toFixed(1)}k`:"0",Uo=({node:e,nodeIndex:t,hasChildren:n,isCollapsed:r,handleTreeNodeClick:o,handleTreeNodeToggle:i,searchValue:a})=>{var s,l,c;const d=Xe(null),u=Xe(null!=(l=null==(s=e.renderData)?void 0:s.renderCount)?l:0),{highlightedText:p,typeHighlight:h}=((e,t)=>Ke(()=>{const{query:n,matches:r}=t,o=r.some(t=>t.nodeId===e.nodeId),i=Po(n)||[],a=n?n.replace(/\[.*?\]/,"").trim():"";if(!n||!o)return{highlightedText:Rn("span",{className:"truncate",children:e.label}),typeHighlight:!1};let s=!0;if(i.length>0)if(e.fiber){const{wrapperTypes:t}=mo(e.fiber);s=Io(i,t)}else s=!1;let l=Rn("span",{className:"truncate",children:e.label});if(a)try{if(a.startsWith("/")&&a.endsWith("/")){const t=a.slice(1,-1),n=new RegExp(`(${t})`,"i"),r=e.label.split(n);l=Rn("span",{className:"tree-node-search-highlight",children:r.map((t,o)=>n.test(t)?Rn("span",{className:lo("regex",{start:n.test(t)&&0===o,middle:n.test(t)&&o%2==1,end:n.test(t)&&o===r.length-1,"!ml-0":1===o}),children:t},`${e.nodeId}-${t}`):t)})}else{const t=e.label.toLowerCase(),n=a.toLowerCase(),r=t.indexOf(n);r>=0&&(l=Rn("span",{className:"tree-node-search-highlight",children:[e.label.slice(0,r),Rn("span",{className:"single",children:e.label.slice(r,r+a.length)}),e.label.slice(r+a.length)]}))}}catch{}return{highlightedText:l,typeHighlight:s&&i.length>0}},[e.label,e.nodeId,e.fiber,t]))(e,a);Je(()=>{var t;const n=null==(t=e.renderData)?void 0:t.renderCount,r=d.current;r&&u.current&&n&&u.current!==n&&(r.classList.remove("count-flash"),r.offsetWidth,r.classList.add("count-flash"),u.current=n)},[null==(c=e.renderData)?void 0:c.renderCount]);const m=Ke(()=>{if(!e.renderData)return null;const{selfTime:t,totalTime:n,renderCount:r}=e.renderData;return r?Rn("span",{className:lo("flex items-center gap-x-0.5 ml-1.5","text-[10px] text-neutral-400"),children:Rn("span",{ref:d,title:`Self time: ${Wo(t)}ms\nTotal time: ${Wo(n)}ms`,className:"count-badge",children:["×",r]})}):null},[e.renderData]),f=Ke(()=>{if(!e.fiber)return null;const{wrapperTypes:t}=mo(e.fiber),n=t[0];return Rn("span",{className:lo("flex items-center gap-x-1","text-[10px] text-neutral-400 tracking-wide","overflow-hidden"),children:[n&&Rn(me,{children:[Rn("span",{title:null==n?void 0:n.title,className:lo("rounded py-[1px] px-1","bg-neutral-700 text-neutral-300","truncate","memo"===n.type&&"bg-[#8e61e3] text-white",h&&"bg-yellow-300 text-black"),children:n.type},n.type),n.compiler&&Rn("span",{className:"text-yellow-300 ml-1",children:"✨"})]}),t.length>1&&`×${t.length}`,m]})},[e.fiber,h,m]);return Rn("button",{type:"button",title:e.title,"data-index":t,className:lo("flex items-center gap-x-1","pl-1 pr-2","w-full h-7","text-left","rounded","cursor-pointer select-none"),onClick:o,children:[Rn("button",{type:"button","data-index":t,onClick:i,className:lo("w-6 h-6 flex items-center justify-center","text-left"),children:n&&Rn(On,{name:"icon-chevron-right",size:12,className:lo("transition-transform",!r&&"rotate-90")})}),p,f]})},Ho=()=>{const e=Xe(null),t=Xe(null),n=Xe(null),r=Xe(null),o=Xe(null),i=Xe(0),a=Xe(!1),s=Xe(!1),l=Xe(null),[c,d]=Ge([]),[u,p]=Ge(new Set),[h,m]=Ge(void 0),[f,g]=Ge(Oo.value),v=Ke(()=>{const e=[],t=c,n=new Map(t.map(e=>[e.nodeId,e]));for(const r of t){let t=!0,o=r;for(;o.parentId;){const e=n.get(o.parentId);if(!e)break;if(u.has(e.nodeId)){t=!1;break}o=e}t&&e.push(r)}return e},[u,c]),w=28,{virtualItems:b,totalSize:x}=(e=>{const{count:t,getScrollElement:n,estimateSize:r,overscan:o=5}=e,[i,a]=Ge(0),[s,l]=Ge(0),c=Xe(),d=Xe(null),u=Xe(null),p=r(),h=Ze(e=>{var t,n;if(!d.current)return;const r=null!=(n=null==(t=null==e?void 0:e[0])?void 0:t.contentRect.height)?n:d.current.getBoundingClientRect().height;l(r)},[]),m=Ze(()=>{null!==u.current&&cancelAnimationFrame(u.current),u.current=requestAnimationFrame(()=>{h(),u.current=null})},[h]);Je(()=>{const e=n();if(!e)return;d.current=e;const t=()=>{d.current&&a(d.current.scrollTop)};h(),c.current||(c.current=new ResizeObserver(()=>{m()})),c.current.observe(e),e.addEventListener("scroll",t,{passive:!0});const r=new MutationObserver(m);return r.observe(e,{attributes:!0,childList:!0,subtree:!0}),()=>{e.removeEventListener("scroll",t),c.current&&c.current.disconnect(),r.disconnect(),null!==u.current&&cancelAnimationFrame(u.current)}},[n,h,m]);const f=Ke(()=>{const e=Math.floor(i/p),n=Math.ceil(s/p);return{start:Math.max(0,e-o),end:Math.min(t,e+n+o)}},[i,p,s,t,o]);return{virtualItems:Ke(()=>{const e=[];for(let t=f.start;te.current,estimateSize:()=>w,overscan:5}),y=Ze(t=>{var n;a.current=!0,null==(n=r.current)||n.blur(),Do.value=!0;const{parentCompositeFiber:o}=yi(t);if(!o)return;Lu.inspectState.value={kind:"focused",focusedDomElement:t,fiber:o};const i=v.findIndex(e=>e.element===t);if(-1!==i){m(i);const t=i*w,n=e.current;if(n){const e=n.clientHeight,r=n.scrollTop;(tr+e)&&n.scrollTo({top:Math.max(0,t-e/2),behavior:"instant"})}}},[v]),k=Ze(e=>{const t=e.currentTarget,n=Number(t.dataset.index);if(Number.isNaN(n))return;const r=v[n].element;r&&y(r)},[v,y]),_=Ze(e=>{p(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),N=Ze(e=>{e.stopPropagation();const t=e.target,n=Number(t.dataset.index);if(Number.isNaN(n))return;const r=v[n].nodeId;_(r)},[v,_]),S=Ze(t=>{var r,o,i,a,s;null==(r=n.current)||r.classList.remove("!border-red-500");const l=[];if(!t)return void(Oo.value={query:t,matches:l,currentMatchIndex:-1});if(t.includes("[")&&!t.includes("]")&&t.length>t.indexOf("[")+1)return void(null==(o=n.current)||o.classList.add("!border-red-500"));const d=Po(t)||[];if(t.includes("[")&&!(e=>{if(0===e.length)return!1;for(const t of e){let e=!1;for(const n of Lo)if(n.toLowerCase().includes(t)){e=!0;break}if(!e)return!1}return!0})(d))return void(null==(i=n.current)||i.classList.add("!border-red-500"));const u=t.replace(/\[.*?\]/,"").trim(),p=/^\/.*\/$/.test(u);let h=e=>!1;if(u.startsWith("/")&&!p&&u.length>1)null==(a=n.current)||a.classList.add("!border-red-500");else{if(p)try{const e=u.slice(1,-1),t=new RegExp(e,"i");h=e=>t.test(e)}catch{return void(null==(s=n.current)||s.classList.add("!border-red-500"))}else if(u){const e=u.toLowerCase();h=t=>t.toLowerCase().includes(e)}for(const e of c){let t=!0;if(u&&(t=h(e.label)),t&&d.length>0)if(e.fiber){const{wrapperTypes:n}=mo(e.fiber);t=Io(d,n)}else t=!1;t&&l.push(e)}if(Oo.value={query:t,matches:l,currentMatchIndex:l.length>0?0:-1},l.length>0){const t=l[0],n=v.findIndex(e=>e.nodeId===t.nodeId);if(-1!==n){const t=n*w,r=e.current;if(r){const e=r.clientHeight;r.scrollTo({top:Math.max(0,t-e/2),behavior:"instant"})}}}}},[c,v]),C=Ze(e=>{const t=e.currentTarget;t&&S(t.value)},[S]),T=Ze(t=>{const{matches:n,currentMatchIndex:r}=Oo.value;if(0===n.length)return;const o="next"===t?(r+1)%n.length:(r-1+n.length)%n.length;Oo.value={...Oo.value,currentMatchIndex:o};const i=n[o],a=v.findIndex(e=>e.nodeId===i.nodeId);if(-1!==a){m(a);const t=a*w,n=e.current;if(n){const e=n.clientHeight;n.scrollTo({top:Math.max(0,t-e/2),behavior:"instant"})}}},[v]),E=Ze(n=>{if(t.current&&(t.current.style.width=`${n}px`),e.current){e.current.style.width=`${n}px`;const t=((e,t)=>{if(t<=0)return 24;const n=Math.max(0,e-In);if(n<24)return 0;const r=Math.min(.3*n,24*t)/t;return Math.max(0,Math.min(24,r))})(n,i.current);e.current.style.setProperty("--indentation-size",`${t}px`)}},[]),z=Ze(e=>{if(!l.current)return;const t=yo.value.dimensions.width,n=Math.floor(t-120);l.current.classList.remove("cursor-ew-resize","cursor-w-resize","cursor-e-resize"),e<=In?l.current.classList.add("cursor-w-resize"):e>=n?l.current.classList.add("cursor-e-resize"):l.current.classList.add("cursor-ew-resize")},[]),A=Ze(t=>{if(t.preventDefault(),t.stopPropagation(),!e.current)return;e.current.style.setProperty("pointer-events","none"),s.current=!0;const n=t.clientX,r=e.current.offsetWidth,o=yo.value.dimensions.width,i=Math.floor(o-120);z(r);const a=e=>{const t=n-e.clientX,o=r+t;z(o);const a=Math.min(i,Math.max(In,o));E(a)},l=()=>{e.current&&(e.current.style.removeProperty("pointer-events"),document.removeEventListener("pointermove",a),document.removeEventListener("pointerup",l),yo.value={...yo.value,componentsTree:{...yo.value.componentsTree,width:e.current.offsetWidth}},po(Wn,yo.value),s.current=!1)};document.addEventListener("pointermove",a),document.addEventListener("pointerup",l)},[E,z]);Je(()=>{if(!e.current)return;const t=e.current.offsetWidth;return z(t),yo.subscribe(()=>{e.current&&z(e.current.offsetWidth)})},[z]);const M=Ze(()=>{a.current=!1},[]);return Je(()=>{let t=!0;const n=()=>{const n=o.current;if(!n)return;const r=(e=>{const t=new Map,n=[];for(const{element:n,name:r,fiber:o}of e){if(!n)continue;let e=r;const{name:i,wrappers:a}=mo(o);i&&(e=a.length>0?`${a.join("(")}(${i})${")".repeat(a.length)}`:i),t.set(n,{label:i||r,title:e,children:[],element:n,fiber:o})}for(const{element:r,depth:o}of e){if(!r)continue;const e=t.get(r);if(e)if(0===o)n.push(e);else{let n=r.parentElement;for(;n;){const r=t.get(n);if(r){r.children=r.children||[],r.children.push(e);break}n=n.parentElement}}}return n})(Ci());if(r.length>0){const o=jo(r),a=o.reduce((e,t)=>Math.max(e,t.depth),0);if(i.current=a,E(yo.value.componentsTree.width),d(o),t){t=!1;const r=o.findIndex(e=>e.element===n);if(-1!==r){const t=r*w,n=e.current;n&&setTimeout(()=>{n.scrollTo({top:t,behavior:"instant"})},96)}}}},r=Lu.inspectState.subscribe(e=>{if("focused"===e.kind){if(Do.value)return;S(""),o.current=e.focusedDomElement,n()}});let a=0;const l=Ao.subscribe(()=>{if("focused"===Lu.inspectState.value.kind){if(cancelAnimationFrame(a),s.current)return;a=requestAnimationFrame(()=>{Do.value=!1,n()})}});return()=>{r(),l(),Oo.value={query:"",matches:[],currentMatchIndex:-1}}},[]),Je(()=>{const e=e=>{if(a.current&&h)switch(e.key){case"ArrowUp":if(e.preventDefault(),e.stopPropagation(),h>0){const e=v[h-1];(null==e?void 0:e.element)&&y(e.element)}return;case"ArrowDown":if(e.preventDefault(),e.stopPropagation(),h{document.removeEventListener("keydown",e)}},[h,v,y,_]),Je(()=>Oo.subscribe(g),[]),Je(()=>yo.subscribe(e=>{var n;null==(n=t.current)||n.style.setProperty("transition","width 0.1s"),E(e.componentsTree.width),setTimeout(()=>{var e;null==(e=t.current)||e.style.removeProperty("transition")},500)}),[]),Rn("div",{className:"react-scan-components-tree flex",children:[Rn("div",{ref:l,onPointerDown:A,className:"relative resize-v-line",children:Rn("span",{children:Rn(On,{name:"icon-ellipsis",size:18})})}),Rn("div",{ref:t,className:"flex flex-col h-full",children:[Rn("div",{className:"p-2 border-b border-[#1e1e1e]",children:Rn("div",{ref:n,title:'Search components by:\n\n• Name (e.g., "Button") — Case insensitive, matches any part\n\n• Regular Expression (e.g., "/^Button/") — Use forward slashes\n\n• Wrapper Type (e.g., "[memo,forwardRef]"):\n - Available types: memo, forwardRef, lazy, suspense\n - Matches any part of type name (e.g., "mo" matches "memo")\n - Use commas for multiple types\n\n• Combined Search:\n - Mix name/regex with type: "button [for]"\n - Will match components satisfying both conditions\n\n• Navigation:\n - Enter → Next match\n - Shift + Enter → Previous match\n - Cmd/Ctrl + Enter → Select and focus match\n',className:lo("relative","flex items-center gap-x-1 px-2","rounded","border border-transparent","focus-within:border-[#454545]","bg-[#1e1e1e] text-neutral-300","transition-colors","whitespace-nowrap","overflow-hidden"),children:[Rn(On,{name:"icon-search",size:12,className:" text-neutral-500"}),Rn("div",{className:"relative flex-1 h-7 overflow-hidden",children:Rn("input",{ref:r,type:"text",value:Oo.value.query,onClick:e=>{e.stopPropagation(),e.currentTarget.focus()},onPointerDown:e=>{e.stopPropagation()},onKeyDown:e=>{"Escape"===e.key&&e.currentTarget.blur(),Oo.value.matches.length&&("Enter"===e.key&&e.shiftKey?T("prev"):"Enter"===e.key&&(e.metaKey||e.ctrlKey?(e.preventDefault(),e.stopPropagation(),y(Oo.value.matches[Oo.value.currentMatchIndex].element),e.currentTarget.focus()):T("next")))},onChange:C,className:"absolute inset-y-0 inset-x-1",placeholder:"Component name, /regex/, or [type]"})}),Oo.value.query?Rn(me,{children:[Rn("span",{className:"flex items-center gap-x-0.5 text-xs text-neutral-500",children:[Oo.value.currentMatchIndex+1,"|",Oo.value.matches.length]}),!!Oo.value.matches.length&&Rn(me,{children:[Rn("button",{type:"button",onClick:e=>{e.stopPropagation(),T("prev")},className:"button rounded w-4 h-4 flex items-center justify-center text-neutral-400 hover:text-neutral-300",children:Rn(On,{name:"icon-chevron-right",className:"-rotate-90",size:12})}),Rn("button",{type:"button",onClick:e=>{e.stopPropagation(),T("next")},className:"button rounded w-4 h-4 flex items-center justify-center text-neutral-400 hover:text-neutral-300",children:Rn(On,{name:"icon-chevron-right",className:"rotate-90",size:12})})]}),Rn("button",{type:"button",onClick:e=>{e.stopPropagation(),S("")},className:"button rounded w-4 h-4 flex items-center justify-center text-neutral-400 hover:text-neutral-300",children:Rn(On,{name:"icon-close",size:12})})]}):!!c.length&&Rn("span",{className:"text-xs text-neutral-500",children:c.length})]})}),Rn("div",{className:"flex-1 overflow-hidden",children:Rn("div",{ref:e,onPointerLeave:M,className:"tree h-full overflow-auto will-change-transform",children:Rn("div",{className:"relative w-full",style:{height:x},children:b.map(e=>{var t;const n=v[e.index];if(!n)return null;const r="focused"===Lu.inspectState.value.kind&&n.element===Lu.inspectState.value.focusedDomElement,o=e.index===h;return Rn("div",{className:lo("absolute left-0 w-full overflow-hidden","text-neutral-400 hover:text-neutral-300","bg-transparent hover:bg-[#5f3f9a]/20",(r||o)&&"text-neutral-300 bg-[#5f3f9a]/40 hover:bg-[#5f3f9a]/40"),style:{top:e.start,height:w},children:Rn("div",{className:"w-full h-full",style:{paddingLeft:`calc(${n.depth} * var(--indentation-size))`},children:Rn(Uo,{node:n,nodeIndex:e.index,hasChildren:!!(null==(t=n.children)?void 0:t.length),isCollapsed:u.has(n.nodeId),handleTreeNodeClick:k,handleTreeNodeToggle:N,searchValue:f})})},n.nodeId)})})})})]})]})},Bo=new WeakMap,Vo={activeFlashes:new Map,create(e){const t=e.querySelector(".react-scan-flash-overlay"),n=t instanceof HTMLElement?t:(()=>{const t=document.createElement("div");t.className="react-scan-flash-overlay",e.appendChild(t);const n=((e,t)=>{const n=t.bind(null,e);return document.addEventListener("scroll",n,{passive:!0,capture:!0}),()=>{document.removeEventListener("scroll",n,{capture:!0})}})(e,()=>{e.querySelector(".react-scan-flash-overlay")&&this.create(e)});return this.activeFlashes.set(e,{element:e,overlay:t,scrollCleanup:n}),t})(),r=Bo.get(n);r&&(clearTimeout(r),Bo.delete(n)),requestAnimationFrame(()=>{n.style.transition="none",n.style.opacity="0.9";const t=setTimeout(()=>{n.style.transition="opacity 150ms ease-out",n.style.opacity="0";const t=setTimeout(()=>{n.parentNode&&n.parentNode.removeChild(n);const t=this.activeFlashes.get(e);(null==t?void 0:t.scrollCleanup)&&t.scrollCleanup(),this.activeFlashes.delete(e),Bo.delete(n)},150);Bo.set(n,t)},300);Bo.set(n,t)})},cleanup(e){const t=this.activeFlashes.get(e);if(t){const n=Bo.get(t.overlay);n&&(clearTimeout(n),Bo.delete(t.overlay)),t.overlay.parentNode&&t.overlay.parentNode.removeChild(t.overlay),t.scrollCleanup&&t.scrollCleanup(),this.activeFlashes.delete(e)}},cleanupAll(){for(const[,e]of this.activeFlashes)this.cleanup(e.element)}},qo=ln(({text:e,children:t,onCopy:n,className:r,iconSize:o=14})=>{const[i,a]=Ge(!1);Je(()=>{if(i){const e=setTimeout(()=>a(!1),600);return()=>{clearTimeout(e)}}},[i]);const s=Ze(t=>{t.preventDefault(),t.stopPropagation(),navigator.clipboard.writeText(e).then(()=>{a(!0),null==n||n(!0,e)},()=>{null==n||n(!1,e)})},[e,n]),l=Rn("button",{onClick:s,type:"button",className:lo("z-10","flex items-center justify-center","hover:text-dev-pink-400","transition-colors duration-200 ease-in-out","cursor-pointer",`size-[${o}px]`,r),children:Rn(On,{name:"icon-"+(i?"check":"copy"),size:[o],className:lo(i&&"text-green-500")})});return t?t({ClipboardIcon:l,onClick:s}):l}),Go=({length:e,expanded:t,onToggle:n,isNegative:r})=>Rn("div",{className:"flex items-center gap-1",children:[Rn("button",{type:"button",onClick:n,className:"flex items-center p-0 opacity-50",children:Rn(On,{name:"icon-chevron-right",size:12,className:lo("transition-[color,transform]",r?"text-[#f87171]":"text-[#4ade80]",t&&"rotate-90")})}),Rn("span",{children:["Array(",e,")"]})]}),Jo=({value:e,path:t,isNegative:n})=>{const[r,o]=Ge(!1);if(!(null!==e&&"object"==typeof e&&!(e instanceof Date)))return Rn("div",{className:"flex items-center gap-1",children:[Rn("span",{className:"text-gray-500",children:[t,":"]}),Rn("span",{className:"truncate",children:Mi(e)})]});const i=Object.entries(e);return Rn("div",{className:"flex flex-col",children:[Rn("div",{className:"flex items-center gap-1",children:[Rn("button",{type:"button",onClick:()=>o(!r),className:"flex items-center p-0 opacity-50",children:Rn(On,{name:"icon-chevron-right",size:12,className:lo("transition-[color,transform]",n?"text-[#f87171]":"text-[#4ade80]",r&&"rotate-90")})}),Rn("span",{className:"text-gray-500",children:[t,":"]}),!r&&Rn("span",{className:"truncate",children:e instanceof Date?Mi(e):`{${Object.keys(e).join(", ")}}`})]}),r&&Rn("div",{className:"pl-5 border-l border-[#333] mt-0.5 ml-1 flex flex-col gap-0.5",children:i.map(([e,t])=>Rn(Jo,{value:t,path:e,isNegative:n},e))})]})},Yo=({value:e,expanded:t,onToggle:n,isNegative:r})=>{const{value:o,error:i}=$i(e);if(i)return Rn("span",{className:"text-gray-500 font-italic",children:i});return null!==o&&"object"==typeof o&&!(o instanceof Promise)?Array.isArray(o)?Rn("div",{className:"flex flex-col gap-1 relative",children:[Rn(Go,{length:o.length,expanded:t,onToggle:n,isNegative:r}),t&&Rn("div",{className:"pl-2 border-l border-[#333] mt-0.5 ml-1 flex flex-col gap-0.5",children:o.map((e,t)=>Rn(Jo,{value:e,path:t.toString(),isNegative:r},t.toString()))}),Rn(qo,{text:Ti(o),className:"absolute top-0.5 right-0.5 opacity-0 transition-opacity group-hover:opacity-100 self-end",children:({ClipboardIcon:e})=>Rn(me,{children:e})})]}):Rn("div",{className:"flex items-start gap-1 relative",children:[Rn("button",{type:"button",onClick:n,className:lo("flex items-center","p-0 mt-0.5 mr-1","opacity-50"),children:Rn(On,{name:"icon-chevron-right",size:12,className:lo("transition-[color,transform]",r?"text-[#f87171]":"text-[#4ade80]",t&&"rotate-90")})}),Rn("div",{className:"flex-1",children:t?Rn("div",{className:"pl-2 border-l border-[#333] mt-0.5 ml-1 flex flex-col gap-0.5",children:Object.entries(o).map(([e,t])=>Rn(Jo,{value:t,path:e,isNegative:r},e))}):Rn("span",{children:Mi(o)})}),Rn(qo,{text:Ti(o),className:"absolute top-0.5 right-0.5 opacity-0 transition-opacity group-hover:opacity-100 self-end",children:({ClipboardIcon:e})=>Rn(me,{children:e})})]}):Rn("span",{children:Mi(o)})},Xo=e=>{switch(e.kind){case"initialized":return e.changes.currentValue;case"partially-initialized":return e.value}},Ko=(e,t)=>{for(const n of e){const e=t.get(n.name);e?t.set(e.name,{count:e.count+1,currentValue:n.value,id:e.name,lastUpdated:Date.now(),name:e.name,previousValue:n.prevValue}):t.set(n.name,{count:1,currentValue:n.value,id:n.name,lastUpdated:Date.now(),name:n.name,previousValue:n.prevValue})}},Zo=e=>{const t={contextChanges:new Map,propsChanges:new Map,stateChanges:new Map};return e.forEach(e=>{((e,t)=>{for(const n of e){const e=t.contextChanges.get(n.contextType);if(e){if(en(Xo(e),n.value))continue;if("partially-initialized"===e.kind){t.contextChanges.set(n.contextType,{kind:"initialized",changes:{count:1,currentValue:n.value,id:n.contextType.toString(),lastUpdated:Date.now(),name:n.name,previousValue:e.value}});continue}t.contextChanges.set(n.contextType,{kind:"initialized",changes:{count:e.changes.count+1,currentValue:n.value,id:n.contextType.toString(),lastUpdated:Date.now(),name:n.name,previousValue:e.changes.currentValue}})}else t.contextChanges.set(n.contextType,{kind:"partially-initialized",id:n.contextType.toString(),lastUpdated:Date.now(),name:n.name,value:n.value})}})(e.contextChanges,t),Ko(e.stateChanges,t.stateChanges),Ko(e.propsChanges,t.propsChanges)}),t},Qo=(e,t)=>{const n=new Map;return e.forEach((e,t)=>{n.set(t,e)}),t.forEach((e,t)=>{const r=n.get(t);r?n.set(t,{count:r.count+e.count,currentValue:e.currentValue,id:e.id,lastUpdated:e.lastUpdated,name:e.name,previousValue:e.previousValue}):n.set(t,e)}),n},ei=(e,t)=>{const n=((e,t)=>{const n=new Map;return e.contextChanges.forEach((e,t)=>{n.set(t,e)}),t.contextChanges.forEach((e,t)=>{const r=n.get(t);if(r){if(Xo(e)!==Xo(r))switch(r.kind){case"initialized":switch(e.kind){case"initialized":{const o=1;return void n.set(t,{kind:"initialized",changes:{...e.changes,count:e.changes.count+r.changes.count+o,currentValue:e.changes.currentValue,previousValue:e.changes.previousValue}})}case"partially-initialized":return void n.set(t,{kind:"initialized",changes:{count:r.changes.count+1,currentValue:e.value,id:e.id,lastUpdated:e.lastUpdated,name:e.name,previousValue:r.changes.currentValue}})}case"partially-initialized":switch(e.kind){case"initialized":return void n.set(t,{kind:"initialized",changes:{count:e.changes.count+1,currentValue:e.changes.currentValue,id:e.changes.id,lastUpdated:e.changes.lastUpdated,name:e.changes.name,previousValue:r.value}});case"partially-initialized":return void n.set(t,{kind:"initialized",changes:{count:1,currentValue:e.value,id:e.id,lastUpdated:e.lastUpdated,name:e.name,previousValue:r.value}})}}}else n.set(t,e)}),n})(e,t);return{contextChanges:n,propsChanges:Qo(e.propsChanges,t.propsChanges),stateChanges:Qo(e.stateChanges,t.stateChanges)}},ti=e=>Array.from(e.propsChanges.values()).reduce((e,t)=>e+t.count,0)+Array.from(e.stateChanges.values()).reduce((e,t)=>e+t.count,0)+Array.from(e.contextChanges.values()).filter(e=>"initialized"===e.kind).reduce((e,t)=>e+t.changes.count,0),ni=ln(()=>{const[e,t]=Ge(!0),n=(()=>{const e=Xe({queue:[]}),[t,n]=Ge({propsChanges:new Map,stateChanges:new Map,contextChanges:new Map}),r="focused"===Lu.inspectState.value.kind?Lu.inspectState.value.fiber:null,o=r?Z(r):null;return Je(()=>{const t=setInterval(()=>{0!==e.current.queue.length&&(n(t=>{const n=Zo(e.current.queue),r=ei(t,n),o=ti(t);ti(r);return r}),e.current.queue=[])},50);return()=>{clearInterval(t)}},[r]),Je(()=>{if(!o)return;const t=t=>{var n;null==(n=e.current)||n.queue.push(t)};let r=Lu.changesListeners.get(o);return r||(r=[],Lu.changesListeners.set(o,r)),r.push(t),()=>{var r,i;n({propsChanges:new Map,stateChanges:new Map,contextChanges:new Map}),e.current.queue=[],Lu.changesListeners.set(o,null!=(i=null==(r=Lu.changesListeners.get(o))?void 0:r.filter(e=>e!==t))?i:[])}},[o]),Je(()=>()=>{n({propsChanges:new Map,stateChanges:new Map,contextChanges:new Map}),e.current.queue=[]},[o]),t})(),[r,o]=Ge(!1),i=ti(n)>0;Je(()=>{if(!r&&i){const e=setTimeout(()=>{o(!0),requestAnimationFrame(()=>{t(!0)})},0);return()=>clearTimeout(e)}},[r,i]);const a=new Map(Array.from(n.contextChanges.entries()).filter(([,e])=>"initialized"===e.kind).map(([e,t])=>[e,"partially-initialized"===t.kind?null:t.changes])),s="focused"===Lu.inspectState.value.kind?Lu.inspectState.value.fiber:null;if(s)return Rn(me,{children:[Rn(oi,{}),Rn("div",{className:"overflow-hidden h-full flex flex-col gap-y-2",children:[Rn("div",{className:"flex flex-col gap-2 px-3 pt-2",children:[Rn("span",{className:"text-sm font-medium text-[#888]",children:["Why did ",Rn("span",{className:"text-[#A855F7]",children:J(s)})," render?"]}),!i&&Rn("div",{className:"text-sm text-[#737373] bg-[#1E1E1E] rounded-md p-4 flex flex-col gap-4",children:[Rn("div",{children:"No changes detected since selecting"}),Rn("div",{children:"The props, state, and context changes within your component will be reported here"})]})]}),Rn("div",{className:lo("flex flex-col gap-y-2 pl-3 relative overflow-y-auto h-full"),children:[Rn(ai,{changes:n.propsChanges,title:"Changed Props",isExpanded:e}),Rn(ai,{renderName:e=>{var t;return ri(e,null!=(t=J(G(s)))?t:"Unknown Component")},changes:n.stateChanges,title:"Changed State",isExpanded:e}),Rn(ai,{changes:a,title:"Changed Context",isExpanded:e})]})]})]})}),ri=(e,t)=>{if(Number.isNaN(Number(e)))return e;const n=Number.parseInt(e);return Rn("span",{className:"truncate",children:[Rn("span",{className:"text-white",children:[n,(e=>{const t=e%100;if(t>=11&&t<=13)return"th";switch(e%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}})(n)," hook"," "]}),Rn("span",{style:{color:"#666"},children:["called in ",Rn("i",{className:"text-[#A855F7] truncate",children:t})]})]})},oi=ln(()=>{const e=Xe(null),t=Xe(null),n=Xe(null),r=Xe({isPropsChanged:!1,isStateChanged:!1,isContextChanged:!1});return Je(()=>{const o=co(()=>{var r,o,i;const a=[];"true"===(null==(r=e.current)?void 0:r.dataset.flash)&&a.push(e.current),"true"===(null==(o=t.current)?void 0:o.dataset.flash)&&a.push(t.current),"true"===(null==(i=n.current)?void 0:i.dataset.flash)&&a.push(n.current);for(const e of a)e.classList.remove("count-flash-white"),e.offsetWidth,e.classList.add("count-flash-white")},400);return zo.subscribe(i=>{var a,s,l,c,d,u,p,h,m;if(!e.current||!t.current||!n.current)return;const{currentIndex:f,updates:g}=i,v=g[f];v&&0!==f&&(o(),r.current={isPropsChanged:(null!=(l=null==(s=null==(a=v.props)?void 0:a.changes)?void 0:s.size)?l:0)>0,isStateChanged:(null!=(u=null==(d=null==(c=v.state)?void 0:c.changes)?void 0:d.size)?u:0)>0,isContextChanged:(null!=(m=null==(h=null==(p=v.context)?void 0:p.changes)?void 0:h.size)?m:0)>0},"true"!==e.current.dataset.flash&&(e.current.dataset.flash=r.current.isPropsChanged.toString()),"true"!==t.current.dataset.flash&&(t.current.dataset.flash=r.current.isStateChanged.toString()),"true"!==n.current.dataset.flash&&(n.current.dataset.flash=r.current.isContextChanged.toString()))})},[]),Rn("button",{type:"button",className:lo("react-section-header","overflow-hidden","max-h-0","transition-[max-height]"),children:Rn("div",{className:lo("flex-1 react-scan-expandable"),children:Rn("div",{className:"overflow-hidden",children:Rn("div",{className:"flex items-center whitespace-nowrap",children:[Rn("div",{className:"flex items-center gap-x-2",children:"What changed?"}),Rn("div",{className:lo("ml-auto","change-scope","transition-opacity duration-300 delay-150"),children:[Rn("div",{ref:e,children:"props"}),Rn("div",{ref:t,children:"state"}),Rn("div",{ref:n,children:"context"})]})]})})})})}),ii=e=>e,ai=ln(({title:e,changes:t,renderName:n=ii})=>{const[r,o]=Ge(new Set),[i,a]=Ge(new Set),s=Array.from(t.entries());return 0===t.size?null:Rn("div",{children:[Rn("div",{className:"text-xs text-[#888] mb-1.5",children:e}),Rn("div",{className:"flex flex-col gap-2",children:s.map(([t,s])=>{const l=i.has(String(t)),{value:c,error:d}=$i(s.previousValue),{value:u,error:p}=$i(s.currentValue),h=Ei(c,u);return Rn("div",{children:[Rn("button",{onClick:()=>{a(e=>{const n=new Set(e);return n.has(String(t))?n.delete(String(t)):n.add(String(t)),n})},className:"flex items-center gap-2 w-full bg-transparent border-none p-0 cursor-pointer text-white text-xs",children:Rn("div",{className:"flex items-center gap-1.5 flex-1",children:[Rn(On,{name:"icon-chevron-right",size:12,className:lo("text-[#666] transition-transform duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]",{"rotate-90":l})}),Rn("div",{className:"whitespace-pre-wrap break-words text-left font-medium flex items-center gap-x-1.5",children:[n(s.name),Rn(di,{count:s.count,isFunction:"function"==typeof s.currentValue,showWarning:0===h.changes.length,forceFlash:!0})]})]})}),Rn("div",{className:lo("react-scan-expandable",{"react-scan-expanded":l}),children:Rn("div",{className:"pl-3 text-xs font-mono border-l-1 border-[#333]",children:Rn("div",{className:"flex flex-col gap-0.5",children:d||p?Rn(si,{currError:p,prevError:d}):h.changes.length>0?Rn(li,{change:s,diff:h,expandedFns:r,renderName:n,setExpandedFns:o,title:e}):Rn(ci,{currValue:u,entryKey:t,expandedFns:r,prevValue:c,setExpandedFns:o})})})})]},t)})})]})}),si=({prevError:e,currError:t})=>Rn(me,{children:[e&&Rn("div",{className:"text-[#f87171] bg-[#2a1515] pr-1.5 py-[3px] rounded italic",children:e}),t&&Rn("div",{className:"text-[#4ade80] bg-[#1a2a1a] pr-1.5 py-[3px] rounded italic mt-0.5",children:t})]}),li=({diff:e,title:t,renderName:n,change:r,expandedFns:o,setExpandedFns:i})=>e.changes.map((a,s)=>{const{value:l,error:c}=$i(a.prevValue),{value:d,error:u}=$i(a.currentValue),p="function"==typeof l||"function"==typeof d;let h;return"Props"===t&&(h=a.path.length>0?`${n(String(r.name))}.${zi(a.path)}`:void 0),"State"===t&&a.path.length>0&&(h=`state.${zi(a.path)}`),h||(h=zi(a.path)),Rn("div",{className:lo("flex flex-col gap-y-1",s{const e=`${zi(a.path)}-prev`;i(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})}:void 0,children:[Rn("span",{className:"w-3 flex items-center justify-center opacity-50",children:"-"}),Rn("span",{className:"flex-1 whitespace-nowrap font-mono",children:c?Rn("span",{className:"italic text-[#f87171]",children:c}):p?Rn("div",{className:"flex gap-1 items-start flex-col",children:[Rn("div",{className:"flex gap-1 items-start w-full",children:[Rn("span",{className:"flex-1 max-h-40",children:Ai(l,o.has(`${zi(a.path)}-prev`))}),"function"==typeof l&&Rn(qo,{text:l.toString(),className:"opacity-0 transition-opacity group-hover:opacity-100",children:({ClipboardIcon:e})=>Rn(me,{children:e})})]}),(null==l?void 0:l.toString())===(null==d?void 0:d.toString())&&Rn("div",{className:"text-[10px] text-[#666] italic",children:"Function reference changed"})]}):Rn(Yo,{value:l,expanded:o.has(`${zi(a.path)}-prev`),onToggle:()=>{const e=`${zi(a.path)}-prev`;i(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},isNegative:!0})})]}),Rn("button",{type:"button",className:lo("group","flex items-start","py-[3px] px-1.5","text-left text-[#4ade80] bg-[#1a2a1a]","rounded","overflow-hidden break-all",p&&"cursor-pointer"),onClick:p?()=>{const e=`${zi(a.path)}-current`;i(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})}:void 0,children:[Rn("span",{className:"w-3 flex items-center justify-center opacity-50",children:"+"}),Rn("span",{className:"flex-1 whitespace-pre-wrap font-mono",children:u?Rn("span",{className:"italic text-[#4ade80]",children:u}):p?Rn("div",{className:"flex gap-1 items-start flex-col",children:[Rn("div",{className:"flex gap-1 items-start w-full",children:[Rn("span",{className:"flex-1",children:Ai(d,o.has(`${zi(a.path)}-current`))}),"function"==typeof d&&Rn(qo,{text:d.toString(),className:"opacity-0 transition-opacity group-hover:opacity-100",children:({ClipboardIcon:e})=>Rn(me,{children:e})})]}),(null==l?void 0:l.toString())===(null==d?void 0:d.toString())&&Rn("div",{className:"text-[10px] text-[#666] italic",children:"Function reference changed"})]}):Rn(Yo,{value:d,expanded:o.has(`${zi(a.path)}-current`),onToggle:()=>{const e=`${zi(a.path)}-current`;i(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},isNegative:!1})})]})]},`${h}-${r.name}-${s}`)}),ci=({prevValue:e,currValue:t,entryKey:n,expandedFns:r,setExpandedFns:o})=>Rn(me,{children:[Rn("div",{className:"group flex gap-0.5 items-start text-[#f87171] bg-[#2a1515] py-[3px] px-1.5 rounded",children:[Rn("span",{className:"w-3 flex items-center justify-center opacity-50",children:"-"}),Rn("span",{className:"flex-1 overflow-hidden whitespace-pre-wrap font-mono",children:Rn(Yo,{value:e,expanded:r.has(`${String(n)}-prev`),onToggle:()=>{const e=`${String(n)}-prev`;o(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},isNegative:!0})})]}),Rn("div",{className:"group flex gap-0.5 items-start text-[#4ade80] bg-[#1a2a1a] py-[3px] px-1.5 rounded mt-0.5",children:[Rn("span",{className:"w-3 flex items-center justify-center opacity-50",children:"+"}),Rn("span",{className:"flex-1 overflow-hidden whitespace-pre-wrap font-mono",children:Rn(Yo,{value:t,expanded:r.has(`${String(n)}-current`),onToggle:()=>{const e=`${String(n)}-current`;o(t=>{const n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},isNegative:!1})})]}),"object"==typeof t&&null!==t&&Rn("div",{className:"text-[#666] text-[10px] italic mt-1 flex items-center gap-x-1",children:[Rn(On,{name:"icon-triangle-alert",className:"text-yellow-500 mb-px",size:14}),Rn("span",{children:"Reference changed but objects are structurally the same"})]})]}),di=({count:e,forceFlash:t,isFunction:n,showWarning:r})=>{const o=Xe(!0),i=Xe(null),a=Xe(e);return Je(()=>{const t=i.current;t&&a.current!==e&&(t.classList.remove("count-flash"),t.offsetWidth,t.classList.add("count-flash"),a.current=e)},[e]),Je(()=>{if(o.current)o.current=!1;else if(t){let e=setTimeout(()=>{var t;null==(t=i.current)||t.classList.add("count-flash-white"),e=setTimeout(()=>{var e;null==(e=i.current)||e.classList.remove("count-flash-white")},300)},500);return()=>{clearTimeout(e)}}},[t]),Rn("div",{ref:i,className:"count-badge",children:[r&&Rn(On,{name:"icon-triangle-alert",className:"text-yellow-500 mb-px",size:14}),n&&Rn(On,{name:"icon-function",className:"text-[#A855F7] mb-px",size:14}),"x",e]})},ui={lastRendered:new Map,expandedPaths:new Set,cleanup:()=>{ui.lastRendered.clear(),ui.expandedPaths.clear(),Vo.cleanupAll(),Wi(),Ro()}},pi=class extends fe{constructor(){super(...arguments),n(this,"state",{hasError:!1,error:null}),n(this,"handleReset",()=>{this.setState({hasError:!1,error:null}),ui.cleanup()})}static getDerivedStateFromError(e){return{hasError:!0,error:e}}render(){var e;return this.state.hasError?Rn("div",{className:"p-4 bg-red-950/50 h-screen backdrop-blur-sm",children:[Rn("div",{className:"flex items-center gap-2 mb-3 text-red-400 font-medium",children:[Rn(On,{name:"icon-flame",className:"text-red-500",size:16}),"Something went wrong in the inspector"]}),Rn("div",{className:"p-3 bg-black/40 rounded font-mono text-xs text-red-300 mb-4 break-words",children:(null==(e=this.state.error)?void 0:e.message)||JSON.stringify(this.state.error)}),Rn("button",{type:"button",onClick:this.handleReset,className:"px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm font-medium transition-colors flex items-center justify-center gap-2",children:"Reset Inspector"})]}):this.props.children}},hi=Et(()=>lo("react-scan-inspector","flex-1","opacity-0","overflow-y-auto overflow-x-hidden","transition-opacity delay-0","pointer-events-none",!wo.value&&"opacity-100 delay-300 pointer-events-auto")),mi=To(()=>{const e=Xe(null),t=t=>{if(!t)return;e.current=t;const{data:n,shouldUpdate:r}=Gi(t);if(r){const e={timestamp:Date.now(),fiberInfo:Ri(t),props:n.fiberProps,state:n.fiberState,context:n.fiberContext,stateNames:Ii(t)};Fo(e,t)}};return Jt(()=>{const n=Lu.inspectState.value;ut(()=>{var r;if("focused"!==n.kind||!n.focusedDomElement)return e.current=null,void ui.cleanup();"focused"===n.kind&&(wo.value=!1);const{parentCompositeFiber:o}=ki(n.focusedDomElement,n.fiber);if(!o)return Lu.inspectState.value={kind:"inspect-off"},void(_o.value={view:"none"});(null==(r=e.current)?void 0:r.type)!==o.type&&(e.current=o,ui.cleanup(),t(o))})}),Jt(()=>{Ao.value,ut(()=>{const n=Lu.inspectState.value;if("focused"!==n.kind||!n.focusedDomElement)return e.current=null,void ui.cleanup();const{parentCompositeFiber:r}=ki(n.focusedDomElement,n.fiber);if(!r)return Lu.inspectState.value={kind:"inspect-off"},void(_o.value={view:"none"});t(r),n.focusedDomElement.isConnected||(e.current=null,ui.cleanup(),Lu.inspectState.value={kind:"inspecting",hoveredDomElement:null})})}),Je(()=>()=>{ui.cleanup()},[]),Rn(pi,{children:Rn("div",{className:hi,children:Rn("div",{className:"w-full h-full",children:Rn(ni,{})})})})}),fi=To(()=>"focused"!==Lu.inspectState.value.kind?null:Rn(pi,{children:[Rn(mi,{}),Rn(Ho,{})]})),gi=e=>{var t,n,r,o;if("__REACT_DEVTOOLS_GLOBAL_HOOK__"in window){const n=window.__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!(null==n?void 0:n.renderers))return null;for(const[,r]of Array.from(n.renderers))try{const n=null==(t=r.findFiberByHostInstance)?void 0:t.call(r,e);if(n)return n}catch{}}if("_reactRootContainer"in e){const t=e._reactRootContainer;return null!=(o=null==(r=null==(n=null==t?void 0:t._internalRoot)?void 0:n.current)?void 0:r.child)?o:null}for(const t in e)if(t.startsWith("__reactInternalInstance$")||t.startsWith("__reactFiber")){return e[t]}return null},vi=e=>{let t=e;for(;t;){if(t.stateNode instanceof Element)return t.stateNode;if(!t.child)break;t=t.child}for(;t;){if(t.stateNode instanceof Element)return t.stateNode;if(!t.return)break;t=t.return}return null},wi=e=>{if(!e)return null;try{const t=gi(e);if(!t)return null;const n=bi(t);return n?n[0]:null}catch{return null}},bi=e=>{let t=e,n=null;for(;t;){if(w(t))return[t,n];v(t)&&!n&&(n=t),t=t.return}return null},xi=(e,t)=>!!k(t,t=>t===e),yi=e=>{const t=wi(e);if(!t)return{};if(!vi(t))return{};const n=bi(t);if(!n)return{};const[r]=n;return{parentCompositeFiber:r}},ki=(e,t)=>{var n,r,o,i;if(!e.isConnected)return{};let a=null!=t?t:wi(e);if(!a)return{};let s=a,l=null,c=null;for(;s;)if(s.stateNode){if(null==(n=Pu.instrumentation)?void 0:n.fiberRoots.has(s.stateNode)){l=s,c=s.stateNode.current;break}s=s.return}else s=s.return;if(!l||!c)return{};if(a=xi(a,c)?a:null!=(r=a.alternate)?r:a,!a)return{};if(!vi(a))return{};const d=null==(o=bi(a))?void 0:o[0];return d?{parentCompositeFiber:xi(d,c)?d:null!=(i=d.alternate)?i:d}:{}},_i=e=>{var t,n,r;const o=null!=(t=e.memoizedProps)?t:{},i=null!=(r=null==(n=e.alternate)?void 0:n.memoizedProps)?r:{},a=[];for(const e in o){if("children"===e)continue;const t=o[e],n=i[e];en(t,n)||a.push({name:e,value:t,prevValue:n,type:1})}return a},Ni=new Set(["HTML","HEAD","META","TITLE","BASE","SCRIPT","SCRIPT","STYLE","LINK","NOSCRIPT","SOURCE","TRACK","EMBED","OBJECT","PARAM","TEMPLATE","PORTAL","SLOT","AREA","XML","DOCTYPE","COMMENT"]),Si=(e,t=!0)=>{if(e.stateNode&&"nodeType"in e.stateNode){const n=e.stateNode;return t&&n.tagName&&Ni.has(n.tagName.toLowerCase())?null:n}let n=e.child;for(;n;){const e=Si(n,t);if(e)return e;n=n.sibling}return null},Ci=(e=document.body)=>{const t=[],n=(e,r=0)=>{var o;const i=(e=>{if(!e)return null;const{parentCompositeFiber:t}=yi(e);return t&&Si(t)===e?e:null})(e);if(i){const{parentCompositeFiber:e}=yi(i);if(!e)return;t.push({element:i,depth:r,name:null!=(o=J(e.type))?o:"Unknown",fiber:e})}for(const t of Array.from(e.children))n(t,i?r+1:r)};return n(e),t},Ti=e=>{try{if(null===e)return"null";if(void 0===e)return"undefined";if(Fi(e))return"Promise";if("function"==typeof e){const t=e.toString();try{return t.replace(/\s+/g," ").replace(/{\s+/g,"{\n ").replace(/;\s+/g,";\n ").replace(/}\s*$/g,"\n}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").replace(/,\s+/g,", ")}catch{return t}}switch(!0){case e instanceof Date:return e.toISOString();case e instanceof RegExp:return e.toString();case e instanceof Error:return`${e.name}: ${e.message}`;case e instanceof Map:return JSON.stringify(Array.from(e.entries()),null,2);case e instanceof Set:return JSON.stringify(Array.from(e),null,2);case e instanceof DataView:return JSON.stringify(Array.from(new Uint8Array(e.buffer)),null,2);case e instanceof ArrayBuffer:return JSON.stringify(Array.from(new Uint8Array(e)),null,2);case ArrayBuffer.isView(e)&&"length"in e:return JSON.stringify(Array.from(e),null,2);case Array.isArray(e):case"object"==typeof e:return JSON.stringify(e,null,2);default:return String(e)}}catch{return String(e)}},Ei=(e,t,n=[],r=new WeakSet)=>{if(e===t)return{type:"primitive",changes:[],hasDeepChanges:!1};if("function"==typeof e&&"function"==typeof t){const r=((e,t)=>{try{return"function"==typeof e&&"function"==typeof t&&e.toString()===t.toString()}catch{return!1}})(e,t);return{type:"primitive",changes:[{path:n,prevValue:e,currentValue:t,sameFunction:r}],hasDeepChanges:!r}}if(null===e||null===t||void 0===e||void 0===t||"object"!=typeof e||"object"!=typeof t)return{type:"primitive",changes:[{path:n,prevValue:e,currentValue:t}],hasDeepChanges:!0};if(r.has(e)||r.has(t))return{type:"object",changes:[{path:n,prevValue:"[Circular]",currentValue:"[Circular]"}],hasDeepChanges:!1};r.add(e),r.add(t);const o=e,i=t,a=new Set([...Object.keys(o),...Object.keys(i)]),s=[];let l=!1;for(const e of a){const t=o[e],a=i[e];if(t!==a)if("object"==typeof t&&"object"==typeof a&&null!==t&&null!==a){const o=Ei(t,a,[...n,e],r);s.push(...o.changes),o.hasDeepChanges&&(l=!0)}else s.push({path:[...n,e],prevValue:t,currentValue:a}),l=!0}return{type:"object",changes:s,hasDeepChanges:l}},zi=e=>0===e.length?"":e.reduce((e,t,n)=>/^\d+$/.test(t)?`${e}[${t}]`:0===n?t:`${e}.${t}`,"");var Ai=(e,t=!1)=>{try{const n=e.toString(),r=n.match(/(?:function\s*)?(?:\(([^)]*)\)|([^=>\s]+))\s*=>?/);if(!r)return"ƒ";const o=(r[1]||r[2]||"").replace(/\s+/g,"");return t?function(e){const t=e.replace(/\s+/g," ").trim(),n=[];let r="";for(let e=0;e"!==t[e+1]?/[(){}[\];,<>:\?!]/.test(o)?(r.trim()&&n.push(r.trim()),n.push(o),r=""):/\s/.test(o)?(r.trim()&&n.push(r.trim()),r=""):r+=o:(r.trim()&&n.push(r.trim()),n.push("=>"),r="",e++)}r.trim()&&n.push(r.trim());const o=[];for(let e=0;e"===r?(o.push(t+r),e++):o.push(t)}const i=new Set,a=new Set;function s(e,t,n){let r=0;for(let i=n;i"===o[t+1])for(let n=e;n<=t;n++)i.add(n)}for(let e=1;e",e);if(-1!==t)for(let n=e;n<=t;n++)a.add(n)}}let l=0;const c=[];let d="";function u(){d.trim()&&c.push(d.replace(/\s+$/,"")),d=""}function p(){u(),d=" ".repeat(l)}const h=[];function m(){return h.length?h[h.length-1]:null}function f(e,t=!1){d.trim()?t||/^[),;:\].}>]$/.test(e)?d+=e:d+=` ${e}`:d+=e}for(let e=0;e"}[t]&&"()"!==n&&"[]"!==n&&"<>"!==n&&(l++,p());else if([")","}","]",">"].includes(t)){const n=m();")"===t&&"("===n||"]"===t&&"["===n||">"===t&&"<"===n?i.has(e)&&")"===t||a.has(e)&&">"===t||(l=Math.max(l-1,0),p()):"}"===t&&"{"===n&&(l=Math.max(l-1,0),p()),h.pop(),f(t),"}"===t&&p()}else if(/^\(\)|\[\]|\{\}|\<\>$/.test(t))f(t);else if("=>"===t)f(t);else if(";"===t)f(t,!0),p();else if(","===t){f(t,!0);const n=m();i.has(e)&&"("===n||a.has(e)&&"<"===n||n&&["{","[","(","<"].includes(n)&&p()}else f(t)}return u(),c.join("\n").replace(/\n\s*\n+/g,"\n").trim()}(n):`ƒ (${o}) => ...`}catch{return"ƒ"}},Mi=e=>{if(null===e)return"null";if(void 0===e)return"undefined";if("string"==typeof e)return`"${e.length>150?`${e.slice(0,20)}...`:e}"`;if("number"==typeof e||"boolean"==typeof e)return String(e);if("function"==typeof e)return Ai(e);if(Array.isArray(e))return`Array(${e.length})`;if(e instanceof Map)return`Map(${e.size})`;if(e instanceof Set)return`Set(${e.size})`;if(e instanceof Date)return e.toISOString();if(e instanceof RegExp)return e.toString();if(e instanceof Error)return`${e.name}: ${e.message}`;if("object"==typeof e){const t=Object.keys(e);return`{${t.length>2?`${t.slice(0,2).join(", ")}, ...`:t.join(", ")}}`}return String(e)},$i=e=>{var t;if(null==e)return{value:e};if("function"==typeof e)return{value:e};if("object"!=typeof e)return{value:e};if(Fi(e))return{value:"Promise"};try{const n=Object.getPrototypeOf(e);return n===Promise.prototype||"Promise"===(null==(t=null==n?void 0:n.constructor)?void 0:t.name)?{value:"Promise"}:{value:e}}catch{return{value:null,error:"Error accessing value"}}},Fi=e=>!!e&&(e instanceof Promise||"object"==typeof e&&"then"in e),Ri=e=>{var t,n;const r=V(e);return{displayName:J(e)||"Unknown",type:e.type,key:e.key,id:e.index,selfTime:null!=(t=null==r?void 0:r.selfTime)?t:null,totalTime:null!=(n=null==r?void 0:r.totalTime)?n:null}},Oi=new Map,Di=new Map,ji=new Map,Li=null,Pi=/\[(?\w+),\s*set\w+\]/g,Ii=e=>{var t,n;const r=(null==(n=null==(t=e.type)?void 0:t.toString)?void 0:n.call(t))||"";return r?Array.from(r.matchAll(Pi),e=>{var t,n;return null!=(n=null==(t=e.groups)?void 0:t.name)?n:""}):[]},Wi=()=>{Oi.clear(),Di.clear(),ji.clear(),Li=null},Ui=(e,t,n,r)=>{const o=e.get(t),i=e===Oi||e===ji,a=!en(n,r);if(!o)return e.set(t,{count:a&&i?1:0,currentValue:n,previousValue:r,lastUpdated:Date.now()}),{hasChanged:a,count:a&&i?1:i?0:1};if(!en(o.currentValue,n)){const r=o.count+1;return e.set(t,{count:r,currentValue:n,previousValue:o.currentValue,lastUpdated:Date.now()}),{hasChanged:!0,count:r}}return{hasChanged:!1,count:o.count}},Hi=e=>{if(!e)return{};if(0===e.tag||11===e.tag||15===e.tag||14===e.tag){let t=e.memoizedState;const n={};let r=0;for(;t;)t.queue&&void 0!==t.memoizedState&&(n[r]=t.memoizedState),t=t.next,r++;return n}return 1===e.tag&&e.memoizedState||{}},Bi=e=>{var t;const n=e.memoizedProps||{},r=(null==(t=e.alternate)?void 0:t.memoizedProps)||{},o={},i={},a=Object.keys(n);for(const e of a)e in n&&(o[e]=n[e],i[e]=r[e]);return{current:o,prev:i,changes:_i(e).map(e=>({name:e.name,value:e.value,prevValue:e.prevValue}))}},Vi=e=>{const t=Hi(e),n=e.alternate?Hi(e.alternate):{},r=[];for(const[o,i]of Object.entries(t)){const t=1===e.tag?o:Number(o);e.alternate&&!en(n[o],i)&&r.push({name:t,value:i,prevValue:n[o]})}return{current:t,prev:n,changes:r}},qi=e=>{const t=Yi(e),n=e.alternate?Yi(e.alternate):new Map,r={},o={},i=[],a=new Set;for(const[e,s]of t){const t=s.displayName,l=e;if(a.has(l))continue;a.add(l),r[t]=s.value;const c=n.get(e);c&&(o[t]=c.value,en(c.value,s.value)||i.push({name:t,value:s.value,prevValue:c.value,contextType:e}))}return{current:r,prev:o,changes:i}},Gi=e=>{const t=()=>({current:[],changes:new Set,changesCounts:new Map});if(!e)return{data:{fiberProps:t(),fiberState:t(),fiberContext:t()},shouldUpdate:!1};let n=!1;const r=(e=>{const t=e.type!==Li;return Li=e.type,t})(e),o=t();if(e.memoizedProps){const{current:t,changes:r}=Bi(e);for(const[e,n]of Object.entries(t))o.current.push({name:e,value:Fi(n)?{type:"promise",displayValue:"Promise"}:n});for(const e of r){const{hasChanged:t,count:r}=Ui(Oi,e.name,e.value,e.prevValue);t&&(n=!0,o.changes.add(e.name),o.changesCounts.set(e.name,r))}}const i=t(),{current:a,changes:s}=Vi(e);for(const[t,n]of Object.entries(a)){const r=1===e.tag?t:Number(t);i.current.push({name:r,value:n})}for(const e of s){const{hasChanged:t,count:r}=Ui(Di,e.name,e.value,e.prevValue);t&&(n=!0,i.changes.add(e.name),i.changesCounts.set(e.name,r))}const l=t(),{current:c,changes:d}=qi(e);for(const[e,t]of Object.entries(c))l.current.push({name:e,value:t});if(!r)for(const e of d){const{hasChanged:t,count:r}=Ui(ji,e.name,e.value,e.prevValue);t&&(n=!0,l.changes.add(e.name),l.changesCounts.set(e.name,r))}return n||r||(o.changes.clear(),i.changes.clear(),l.changes.clear()),{data:{fiberProps:o,fiberState:i,fiberContext:l},shouldUpdate:n||r}},Ji=new WeakMap,Yi=e=>{var t;if(!e)return new Map;const n=Ji.get(e);if(n)return n;const r=new Map;let o=e;for(;o;){const e=o.dependencies;if(null==e?void 0:e.firstContext){let n=e.firstContext;for(;n;){const e=n.memoizedValue,o=null==(t=n.context)?void 0:t.displayName;if(r.has(e)||r.set(n.context,{value:e,displayName:null!=o?o:"UnnamedContext",contextType:null}),n===n.next)break;n=n.next}}o=o.return}return Ji.set(e,r),r},Xi=e=>{const t=()=>({current:[],changes:new Set,changesCounts:new Map});if(!e)return{fiberProps:t(),fiberState:t(),fiberContext:t()};const n=t();if(e.memoizedProps){const{current:t,changes:r}=Bi(e);for(const[e,r]of Object.entries(t))n.current.push({name:e,value:Fi(r)?{type:"promise",displayValue:"Promise"}:r});for(const e of r)n.changes.add(e.name),n.changesCounts.set(e.name,1)}const r=t();if(e.memoizedState){const{current:t,changes:n}=Vi(e);for(const[e,n]of Object.entries(t))r.current.push({name:e,value:Fi(n)?{type:"promise",displayValue:"Promise"}:n});for(const e of n)r.changes.add(e.name),r.changesCounts.set(e.name,1)}const o=t(),{current:i,changes:a}=qi(e);for(const[e,t]of Object.entries(i))o.current.push({name:e,value:Fi(t)?{type:"promise",displayValue:"Promise"}:t});for(const e of a)o.changes.add(e.name),o.changesCounts.set(e.name,1);return{fiberProps:n,fiberState:r,fiberContext:o}},Ki={mount:1,update:2,unmount:4},Zi=0,Qi=performance.now(),ea=0,ta=!1,na=()=>{ea++;const e=performance.now();e-Qi>=1e3&&(Zi=ea,ea=0,Qi=e),requestAnimationFrame(na)},ra=()=>(ta||(ta=!0,na(),Zi=60),Zi),oa=0,ia=new WeakMap,aa=e=>{const t=ia.get(e);return t||(oa++,ia.set(e,oa),oa)};function sa(e,t){var n;if(!e||!t)return;const r=e.memoizedValue,o={type:4,name:null!=(n=e.context.displayName)?n:"Context.Provider",value:r,contextType:aa(e.context)};this.push(o)}var la=e=>{const t=[];return((e,t)=>{var n;try{let r=e.dependencies,o=null==(n=e.alternate)?void 0:n.dependencies;if(!(r&&o&&"object"==typeof r&&"firstContext"in r&&"object"==typeof o&&"firstContext"in o))return!1;let i=r.firstContext,a=o.firstContext;for(;i&&"object"==typeof i&&"memoizedValue"in i||a&&"object"==typeof a&&"memoizedValue"in a;){if(!0===t(i,a))return!0;i=null==i?void 0:i.next,a=null==a?void 0:a.next}}catch{}})(e,sa.bind(t)),t},ca=new Map,da=!1,ua=()=>Array.from(ca.values()),pa=new WeakMap;function ha(e){return String(Z(e))}function ma(e){const t=ha(e),n=pa.get(G(e));if(n)return n.get(t)}var fa=(e,t,n,r,o)=>{const i=Date.now(),a=ma(e);if((r||o)&&(!a||i-(a.lastRenderTimestamp||0)>16)){const r=a||{selfTime:0,totalTime:0,renderCount:0,lastRenderTimestamp:i};r.renderCount=(r.renderCount||0)+1,r.selfTime=t||0,r.totalTime=n||0,r.lastRenderTimestamp=i,function(e,t){const n=G(e.type),r=ha(e);let o=pa.get(n);o||(o=new Map,pa.set(n,o)),o.set(r,t)}(e,{...r})}},ga=(e,t)=>{const n={isPaused:_t(!Pu.options.value.enabled),fiberRoots:new WeakSet};return ca.set(e,{key:e,config:t,instrumentation:n}),da||(da=!0,(e=>{var t;let n=g(e.onActive);n._instrumentationSource=null!=(t=e.name)?t:o;let r=n.onCommitFiberRoot;if(e.onCommitFiberRoot){let t=(n,o,i)=>{var a;r!==t&&(null==r||r(n,o,i),null==(a=e.onCommitFiberRoot)||a.call(e,n,o,i))};n.onCommitFiberRoot=t}let i=n.onCommitFiberUnmount;if(e.onCommitFiberUnmount){let t=(r,o)=>{var a;n.onCommitFiberUnmount===t&&(null==i||i(r,o),null==(a=e.onCommitFiberUnmount)||a.call(e,r,o))};n.onCommitFiberUnmount=t}let a=n.onPostCommitFiberRoot;if(e.onPostCommitFiberRoot){let t=(r,o)=>{var i;n.onPostCommitFiberRoot===t&&(null==a||a(r,o),null==(i=e.onPostCommitFiberRoot)||i.call(e,r,o))};n.onPostCommitFiberRoot=t}})({name:"react-scan",onActive:t.onActive,onCommitFiberRoot(e,t){n.fiberRoots.add(t);const r=ua();for(const e of r)e.config.onCommitStart();((e,t)=>{let n="current"in e?e.current:e,r=oe.get(e);r||(r={id:re++,prevFiber:null},oe.set(e,r));let{prevFiber:o}=r;if(n)if(null!==o){let e=o&&null!=o.memoizedState&&null!=o.memoizedState.element&&!0!==o.memoizedState.isDehydrated,r=null!=n.memoizedState&&null!=n.memoizedState.element&&!0!==n.memoizedState.isDehydrated;!e&&r?Q(t,n,!1):e&&r?ee(t,n,n.alternate):e&&!r&&te(t,n)}else Q(t,n,!0);else te(t,n);r.prevFiber=n})(t.current,(e,t)=>{const n=G(e.type);if(!n)return null;const r=ua(),o=[];for(let t=0,n=r.length;te.config.trackChanges)){const t=Bi(e).changes,n=Vi(e).changes,r=qi(e).changes;i.push.apply(null,t.map(e=>({type:1,name:e.name,value:e.value})));for(const t of n)1===e.tag?i.push({type:3,name:t.name.toString(),value:t.value}):i.push({type:2,name:t.name.toString(),value:t.value});i.push.apply(null,r.map(e=>({type:4,name:e.name,value:e.value,contextType:Number(e.contextType)})))}const{selfTime:a,totalTime:s}=V(e),l=ra(),c={phase:Ki[t],componentName:J(n),count:1,changes:i,time:a,forget:q(e),unnecessary:null,didCommit:x(e),fps:l},d=i.length>0,u=(e=>{let t=[],n=[e];for(;n.length;){let e=n.pop();e&&(v(e)&&x(e)&&b(e)&&t.push(e),e.child&&n.push(e.child),e.sibling&&n.push(e.sibling))}return t})(e).length>0;"update"===t&&fa(e,a,s,d,u);for(let t=0,n=o.length;t{const n=t-e;return Math.abs(n)<.5?t:e+.2*n},wa="115,97,230";function ba(e,t){return t[0]-e[0]}function xa([e,t]){let n=`${t.slice(0,4).join(", ")} ×${e}`;return n.length>40&&(n=`${n.slice(0,40)}…`),n}var ya=e=>{const t=new Map;for(const{name:n,count:r}of e)t.set(n,(t.get(n)||0)+r);const n=new Map;for(const[e,r]of t){const t=n.get(r);t?t.push(e):n.set(r,[e])}const r=function(e){return[...e.entries()].sort(ba)}(n);let o=xa(r[0]);for(let e=1,t=r.length;e40?`${o.slice(0,40)}…`:o},ka=e=>{let t=0;for(const n of e)t+=n.width*n.height;return t},_a=(e,t)=>{for(const{id:n,name:r,count:o,x:i,y:a,width:s,height:l,didCommit:c}of t){const t={id:n,name:r,count:o,x:i,y:a,width:s,height:l,frame:0,targetX:i,targetY:a,targetWidth:s,targetHeight:l,didCommit:c},d=String(t.id),u=e.get(d);u?(u.count++,u.frame=0,u.targetX=i,u.targetY=a,u.targetWidth=s,u.targetHeight=l,u.didCommit=c):e.set(d,t)}},Na=(e,t,n)=>{for(const r of e.values()){const e=r.x-t,o=r.y-n;r.targetX=e,r.targetY=o}},Sa=null,Ca=null,Ta=null,Ea=1,za=null,Aa=new Map,Ma=new Map,$a=new Set,Fa=e=>{if(!w(e))return;const t="string"==typeof e.type?e.type:J(e);if(!t)return;const n=Ma.get(e),r=(e=>{let t=[],n=[];for(v(e)?t.push(e):e.child&&n.push(e.child);n.length;){let e=n.pop();if(!e)break;v(e)?t.push(e):e.child&&n.push(e.child),e.sibling&&n.push(e.sibling)}return t})(e),o=x(e);n?n.count++:(Ma.set(e,{name:t,count:1,elements:r.map(e=>e.stateNode),didCommit:o?1:0}),$a.add(e))},Ra=e=>{const t=e[0];if(1===e.length)return t;let n,r,o,i;for(let t=0,a=e.length;t0&&this.resolveNext&&(this.resolveNext(n),this.resolveNext=null),this.seenElements.size===this.uniqueElements.size&&(t.disconnect(),this.done=!0,this.resolveNext&&this.resolveNext([]))}var Da,ja=async function*(e){const t={uniqueElements:new Set(e),seenElements:new Set,resolveNext:null,done:!1},n=new IntersectionObserver(Oa.bind(t));for(const e of t.uniqueElements)n.observe(e);for(;!t.done;){const e=await new Promise(e=>{t.resolveNext=e});e.length>0&&(yield e)}},La="undefined"!=typeof SharedArrayBuffer?SharedArrayBuffer:ArrayBuffer,Pa=async()=>{const e=[];for(const t of $a){const n=Ma.get(t);if(n)for(let t=0;t0){const t=new La(7*e.length*4),n=new Float32Array(t),i=new Array(e.length);let a;for(let t=0,s=e.length;t{if(!Ta||!Ca)return;const e=((e,t,n,r)=>{e.clearRect(0,0,t.width/n,t.height/n);const o=new Map,i=new Map;for(const e of r.values()){const{x:t,y:n,width:r,height:a,targetX:s,targetY:l,targetWidth:c,targetHeight:d,frame:u}=e;s!==t&&(e.x=va(t,s)),l!==n&&(e.y=va(n,l)),c!==r&&(e.width=va(r,c)),d!==a&&(e.height=va(a,d));const p=`${null!=s?s:t},${null!=l?l:n}`,h=`${p},${null!=c?c:r},${null!=d?d:a}`,m=o.get(p);m?m.push(e):o.set(p,[e]);const f=1-u/45;e.frame++;const g=i.get(h)||{x:t,y:n,width:r,height:a,alpha:f};f>g.alpha&&(g.alpha=f),i.set(h,g)}for(const{x:t,y:n,width:r,height:o,alpha:a}of i.values()){e.strokeStyle=`rgba(${wa},${a})`,e.lineWidth=1;const i=Math.round(t)+.5,s=Math.round(n)+.5,l=Math.round(r),c=Math.round(o);e.beginPath(),e.rect(i,s,l,c),e.stroke(),e.fillStyle=`rgba(${wa},${.1*a})`,e.fill()}e.font="11px Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace";const a=new Map;e.textRendering="optimizeSpeed";for(const t of o.values()){const n=t[0],{x:o,y:i,frame:s}=n,l=1-s/45,c=ya(t),{width:d}=e.measureText(c),u=11;if(a.set(`${o},${i},${d},${c}`,{text:c,width:d,height:u,alpha:l,x:o,y:i,outlines:t}),s>45)for(const e of t)r.delete(String(e.id))}const s=Array.from(a.entries()).sort(([e,t],[n,r])=>ka(r.outlines)-ka(t.outlines));for(const[t,n]of s)if(a.has(t))for(const[r,o]of a.entries()){if(t===r)continue;const{x:i,y:s,width:l,height:c}=n,{x:d,y:u,width:p,height:h}=o;i+l>d&&d+p>i&&s+c>u&&u+h>s&&(n.text=ya(n.outlines.concat(o.outlines)),n.width=e.measureText(n.text).width,a.delete(r))}for(const t of a.values()){const{x:n,y:r,alpha:o,width:i,height:a,text:s}=t;let l=r-a-4;l<0&&(l=0),e.fillStyle=`rgba(${wa},${o})`,e.fillRect(n,l,i+4,a+4),e.fillStyle=`rgba(255,255,255,${o})`,e.fillText(s,n+2,l+a)}return r.size>0})(Ta,Ca,Ea,Aa);za=e?requestAnimationFrame(Ia):null},Wa="undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof Worker,Ua=()=>Math.min(window.devicePixelRatio||1,2),Ha=()=>globalThis.__REACT_SCAN_STOP__,Ba=()=>{const e=document.querySelector("[data-react-scan]");e&&e.remove()},Va=e=>{var t,n;if(w(e)&&!1!==Pu.options.value.showToolbar&&"focused"===Lu.inspectState.value.kind){const r=e,{selfTime:o}=V(e),i=J(e.type),a=Z(r),s=Lu.reportData.get(a),l=null!=(t=null==s?void 0:s.count)?t:0,c=null!=(n=null==s?void 0:s.time)?n:0,d=[],u=Lu.changesListeners.get(Z(e));if(null==u?void 0:u.length){const t=_i(e).map(e=>({type:1,name:e.name,value:e.value,prevValue:e.prevValue,unstable:!1})),n=(e=>{var t,n;if(!e)return[];const r=[];if(0===e.tag||11===e.tag||15===e.tag||14===e.tag){let n=e.memoizedState,o=null==(t=e.alternate)?void 0:t.memoizedState,i=0;for(;n;){if(n.queue&&void 0!==n.memoizedState){const e={type:2,name:i.toString(),value:n.memoizedState,prevValue:null==o?void 0:o.memoizedState};en(e.prevValue,e.value)||r.push(e)}n=n.next,o=null==o?void 0:o.next,i++}return r}if(1===e.tag){const t={type:3,name:"state",value:e.memoizedState,prevValue:null==(n=e.alternate)?void 0:n.memoizedState};return en(t.prevValue,t.value)||r.push(t),r}return r})(e),r=la(e).map(e=>({name:e.name,type:4,value:e.value,contextType:e.contextType}));u.forEach(e=>{e({propsChanges:t,stateChanges:n,contextChanges:r})})}const p={count:l+1,time:c+o||0,renders:[],displayName:i,type:G(e.type)||null,changes:d};Lu.reportData.set(a,p),qa=!0}},qa=!1,Ga=e=>!Xu.has(e.memoizedProps),Ja=!1,Ya=e=>{if(Ha())return;if(Ja)return;let t;Ja=!0;let n=!1;const r=()=>{n||(t&&cancelAnimationFrame(t),t=requestAnimationFrame(()=>{n=!0;const t=(()=>{Ba();const e=document.createElement("div");e.setAttribute("data-react-scan","true");const t=e.attachShadow({mode:"open"}),n=document.createElement("canvas");if(n.style.position="fixed",n.style.top="0",n.style.left="0",n.style.pointerEvents="none",n.style.zIndex="2147483646",n.setAttribute("aria-hidden","true"),t.appendChild(n),!n)return null;Ea=Ua(),Ca=n;const{innerWidth:r,innerHeight:o}=window;n.style.width=`${r}px`,n.style.height=`${o}px`;const i=r*Ea,a=o*Ea;n.width=i,n.height=a;const s=!1===Pu.options.value.useOffscreenCanvasWorker;if(Wa&&!window.__REACT_SCAN_EXTENSION__&&!s)try{const e=URL.createObjectURL(new Blob(['"use strict";(()=>{var D="Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace";var T=(t,n)=>{let r=n-t;return Math.abs(r)<.5?n:t+r*.2};var x="115,97,230";function P(t,n){return n[0]-t[0]}function F(t){return[...t.entries()].sort(P)}function v([t,n]){let r=`${n.slice(0,4).join(", ")} \\xD7${t}`;return r.length>40&&(r=`${r.slice(0,40)}\\u2026`),r}var $=t=>{let n=new Map;for(let{name:e,count:u}of t)n.set(e,(n.get(e)||0)+u);let r=new Map;for(let[e,u]of n){let A=r.get(u);A?A.push(e):r.set(u,[e])}let d=F(r),a=v(d[0]);for(let e=1,u=d.length;e40?`${a.slice(0,40)}\\u2026`:a},H=t=>{let n=0;for(let r of t)n+=r.width*r.height;return n};var N=(t,n)=>{let r=t.getContext("2d",{alpha:!0});return r&&r.scale(n,n),r},X=(t,n,r,d)=>{t.clearRect(0,0,n.width/r,n.height/r);let a=new Map,e=new Map;for(let i of d.values()){let{x:o,y:c,width:l,height:g,targetX:s,targetY:f,targetWidth:h,targetHeight:m,frame:O}=i;s!==o&&(i.x=T(o,s)),f!==c&&(i.y=T(c,f)),h!==l&&(i.width=T(l,h)),m!==g&&(i.height=T(g,m));let M=`${s??o},${f??c}`,L=`${M},${h??l},${m??g}`,S=a.get(M);S?S.push(i):a.set(M,[i]);let C=1-O/45;i.frame++;let _=e.get(L)||{x:o,y:c,width:l,height:g,alpha:C};C>_.alpha&&(_.alpha=C),e.set(L,_)}for(let{x:i,y:o,width:c,height:l,alpha:g}of e.values()){t.strokeStyle=`rgba(${x},${g})`,t.lineWidth=1;let s=Math.round(i)+.5,f=Math.round(o)+.5,h=Math.round(c),m=Math.round(l);t.beginPath(),t.rect(s,f,h,m),t.stroke(),t.fillStyle=`rgba(${x},${g*.1})`,t.fill()}t.font=`11px ${D}`;let u=new Map;t.textRendering="optimizeSpeed";for(let i of a.values()){let o=i[0],{x:c,y:l,frame:g}=o,s=1-g/45,f=$(i),{width:h}=t.measureText(f),m=11;u.set(`${c},${l},${h},${f}`,{text:f,width:h,height:m,alpha:s,x:c,y:l,outlines:i});let O=l-m-4;if(O<0&&(O=0),g>45)for(let M of i)d.delete(String(M.id))}let A=Array.from(u.entries()).sort(([i,o],[c,l])=>H(l.outlines)-H(o.outlines));for(let[i,o]of A)if(u.has(i))for(let[c,l]of u.entries()){if(i===c)continue;let{x:g,y:s,width:f,height:h}=o,{x:m,y:O,width:M,height:L}=l;g+f>m&&m+M>g&&s+h>O&&O+L>s&&(o.text=$(o.outlines.concat(l.outlines)),o.width=t.measureText(o.text).width,u.delete(c))}for(let i of u.values()){let{x:o,y:c,alpha:l,width:g,height:s,text:f}=i,h=c-s-4;h<0&&(h=0),t.fillStyle=`rgba(${x},${l})`,t.fillRect(o,h,g+4,s+4),t.fillStyle=`rgba(255,255,255,${l})`,t.fillText(f,o+2,h+s)}return d.size>0};var p=null,w=null,b=1,y=new Map,E=null,R=()=>{if(!w||!p)return;X(w,p,b,y)?E=requestAnimationFrame(R):E=null};self.onmessage=t=>{let{type:n}=t.data;if(n==="init"&&(p=t.data.canvas,b=t.data.dpr,p&&(p.width=t.data.width,p.height=t.data.height,w=N(p,b))),!(!p||!w)){if(n==="resize"){b=t.data.dpr,p.width=t.data.width*b,p.height=t.data.height*b,w.resetTransform(),w.scale(b,b),R();return}if(n==="draw-outlines"){let{data:r,names:d}=t.data,a=new Float32Array(r);for(let e=0;e{const n=e.getContext("2d",{alpha:!0});return n&&n.scale(t,t),n})(n,Ea));let l=!1;window.addEventListener("resize",()=>{l||(l=!0,setTimeout(()=>{const e=window.innerWidth,t=window.innerHeight;Ea=Ua(),n.style.width=`${e}px`,n.style.height=`${t}px`,Sa?Sa.postMessage({type:"resize",width:e,height:t,dpr:Ea}):(n.width=e*Ea,n.height=t*Ea,Ta&&(Ta.resetTransform(),Ta.scale(Ea,Ea)),Ia()),l=!1}))});let c=window.scrollX,d=window.scrollY,u=!1;return window.addEventListener("scroll",()=>{u||(u=!0,setTimeout(()=>{const{scrollX:e,scrollY:t}=window,n=e-c,r=t-d;c=e,d=t,Sa?Sa.postMessage({type:"scroll",deltaX:n,deltaY:r}):requestAnimationFrame(Na.bind(null,Aa,n,r)),u=!1},32))}),setInterval(()=>{$a.size&&requestAnimationFrame(Pa)},32),t.appendChild(n),e})();t&&document.documentElement.appendChild(t),e()}))},o=ga("react-scan-devtools-0.1.0",{onCommitStart:()=>{var e,t;null==(t=(e=Pu.options.value).onCommitStart)||t.call(e)},onActive:(()=>{let e=!1;return()=>{Ha()||e||(e=!0,r(),window.__REACT_SCAN_EXTENSION__||(globalThis.__REACT_SCAN__={ReactScanInternals:Pu}),clearInterval(Da),Da=setInterval(()=>{qa&&(Lu.lastReportTime.value=Date.now(),qa=!1)},50),window.hideIntro?window.hideIntro=void 0:console.log("%c[·] %cReact Scan","font-weight:bold;color:#7a68e8;font-size:20px;","font-weight:bold;font-size:14px;"))}})(),onError:()=>{},isValidFiber:Ga,onRender:(e,t)=>{var n,r,o,i,a;w(e)&&(null==(r=(n=Lu).interactionListeningForRenders)||r.call(n,e,t));const s=null==(o=Pu.instrumentation)?void 0:o.isPaused.value,l="inspect-off"===Lu.inspectState.value.kind||"uninitialized"===Lu.inspectState.value.kind;s&&l||(s||Fa(e),Pu.options.value.log&&(e=>{var t;const n=new Map;for(let r=0,o=e.length;re|t.type,0),unstable:o.changes.some(e=>e.unstable)},phase:o.phase,computedCurrent:null}]);if(!a)continue;let s=null,l=null;if(o.changes)for(let e=0,t=o.changes.length;e{var e,t;r(),null==(t=(e=Pu.options.value).onCommitFinish)||t.call(e)},onPostCommitFiberRoot(){r()},trackChanges:!1});Pu.instrumentation=o},Xa=null;(()=>{if(null!==Xa)return Xa;try{Xa=window.matchMedia("(color-gamut: p3)").matches}catch{Xa=!1}})();var Ka,Za,Qa,es,ts,ns=new Set("display.position.top.right.bottom.left.z-index.overflow.overflow-x.overflow-y.width.height.min-width.min-height.max-width.max-height.margin-top.margin-right.margin-bottom.margin-left.padding-top.padding-right.padding-bottom.padding-left.flex-direction.flex-wrap.justify-content.align-items.align-self.align-content.flex-grow.flex-shrink.flex-basis.order.gap.row-gap.column-gap.grid-template-columns.grid-template-rows.grid-template-areas.font-family.font-size.font-weight.font-style.line-height.letter-spacing.text-align.text-decoration-line.text-decoration-style.text-transform.text-overflow.text-shadow.white-space.word-break.overflow-wrap.vertical-align.color.background-color.background-image.background-position.background-size.background-repeat.border-top-width.border-right-width.border-bottom-width.border-left-width.border-top-style.border-right-style.border-bottom-style.border-left-style.border-top-color.border-right-color.border-bottom-color.border-left-color.border-top-left-radius.border-top-right-radius.border-bottom-left-radius.border-bottom-right-radius.box-shadow.opacity.transform.filter.backdrop-filter.object-fit.object-position".split(".")),rs=e=>(e.tagName||"").toLowerCase(),os=typeof window<"u",is=os?(null!=(Za=null==(Ka=Object.getOwnPropertyDescriptor(Window.prototype,"requestAnimationFrame"))?void 0:Ka.value)?Za:window.requestAnimationFrame).bind(window):e=>0,as=os?(null!=(es=null==(Qa=Object.getOwnPropertyDescriptor(Window.prototype,"cancelAnimationFrame"))?void 0:Qa.value)?es:window.cancelAnimationFrame).bind(window):e=>{},ss="bippy-0.5.39",ls=Object.defineProperty,cs=Object.prototype.hasOwnProperty,ds=()=>{},us=e=>{try{Function.prototype.toString.call(e).indexOf("^_^")>-1&&setTimeout(()=>{throw Error("React is running in production mode, but dead code elimination has not been applied. Read how to correctly configure React for production: https://reactjs.org/link/perf-use-production-build")})}catch{}},ps=(e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__)=>!(!e||!("getFiberRoots"in e)),hs=!1,ms=(e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__)=>!!hs||(e&&"function"==typeof e.inject&&(ts=e.inject.toString()),!!(null==ts?void 0:ts.includes("(injected)"))),fs=new Set,gs=new Set,vs=e=>{let t=new Map,n=0,r={_instrumentationIsActive:!1,_instrumentationSource:ss,checkDCE:us,hasUnsupportedRendererAttached:!1,inject(e){let o=++n;return t.set(o,e),gs.add(e),r._instrumentationIsActive||(r._instrumentationIsActive=!0,fs.forEach(e=>e())),o},on:ds,onCommitFiberRoot:ds,onCommitFiberUnmount:ds,onPostCommitFiberRoot:ds,renderers:t,supportsFiber:!0,supportsFlight:!0};try{ls(globalThis,"__REACT_DEVTOOLS_GLOBAL_HOOK__",{configurable:!0,enumerable:!0,get:()=>r,set(t){if(t&&"object"==typeof t){let n=r.renderers;r=t,n.size>0&&(n.forEach((e,n)=>{gs.add(e),t.renderers.set(n,e)}),ws(e))}}});let t=window.hasOwnProperty,n=!1;ls(window,"hasOwnProperty",{configurable:!0,value:function(...e){try{if(!n&&"__REACT_DEVTOOLS_GLOBAL_HOOK__"===e[0])return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__=void 0,n=!0,-0}catch{}return t.apply(this,e)},writable:!0})}catch{ws(e)}return r},ws=e=>{try{let t=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!t)return;if(!t._instrumentationSource){t.checkDCE=us,t.supportsFiber=!0,t.supportsFlight=!0,t.hasUnsupportedRendererAttached=!1,t._instrumentationSource=ss,t._instrumentationIsActive=!1;let e=ps(t);if(e||(t.on=ds),t.renderers.size)return t._instrumentationIsActive=!0,void fs.forEach(e=>e());let n=t.inject,r=ms(t);r&&!e&&(hs=!0,t.inject({scheduleRefresh(){}})&&(t._instrumentationIsActive=!0)),t.inject=e=>{let o=n(e);return gs.add(e),r&&t.renderers.set(o,e),t._instrumentationIsActive=!0,fs.forEach(e=>e()),o}}(t.renderers.size||t._instrumentationIsActive||ms())&&(null==e||e())}catch{}},bs=e=>cs.call(globalThis,"__REACT_DEVTOOLS_GLOBAL_HOOK__")?(ws(e),globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__):vs(e),xs=e=>{switch(e.tag){case 1:case 11:case 0:case 14:case 15:return!0;default:return!1}};function ys(e,t,n=!1){if(!e)return null;let r=t(e);if(r instanceof Promise)return(async()=>{if(!0===await r)return e;let o=n?e.return:e.child;for(;o;){let e=await _s(o,t,n);if(e)return e;o=n?null:o.sibling}return null})();if(!0===r)return e;let o=n?e.return:e.child;for(;o;){let e=ks(o,t,n);if(e)return e;o=n?null:o.sibling}return null}var ks=(e,t,n=!1)=>{if(!e)return null;if(!0===t(e))return e;let r=n?e.return:e.child;for(;r;){let e=ks(r,t,n);if(e)return e;r=n?null:r.sibling}return null},_s=async(e,t,n=!1)=>{if(!e)return null;if(!0===await t(e))return e;let r=n?e.return:e.child;for(;r;){let e=await _s(r,t,n);if(e)return e;r=n?null:r.sibling}return null},Ns=e=>{let t=e;return"function"==typeof t?t:"object"==typeof t&&t?Ns(t.type||t.render):null},Ss=e=>{let t=e;if("string"==typeof t)return t;if("function"!=typeof t&&("object"!=typeof t||!t))return null;let n=t.displayName||t.name||null;if(n)return n;let r=Ns(t);return r&&(r.displayName||r.name)||null},Cs=()=>{let e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;return!!(null==e?void 0:e._instrumentationIsActive)||ps(e)||ms(e)},Ts=e=>{var t,n,r,o;let i=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;if(null==i?void 0:i.renderers)for(let n of i.renderers.values())try{let r=null==(t=n.findFiberByHostInstance)?void 0:t.call(n,e);if(r)return r}catch{}if("object"==typeof e&&e){if("_reactRootContainer"in e)return null==(o=null==(r=null==(n=e._reactRootContainer)?void 0:n._internalRoot)?void 0:r.current)?void 0:o.child;for(let t in e)if(t.startsWith("__reactContainer$")||t.startsWith("__reactInternalInstance$")||t.startsWith("__reactFiber"))return e[t]||null}return null},Es=/^[a-zA-Z][a-zA-Z\d+\-.]*:/,zs=["rsc://","file:///","webpack-internal://","webpack://","node:","turbopack://","metro://","/app-pages-browser/","/(app-pages-browser)/"],As=["","eval",""],Ms=/\.(jsx|tsx|ts|js)$/,$s=/(\.min|bundle|chunk|vendor|vendors|runtime|polyfill|polyfills)\.(js|mjs|cjs)$|(chunk|bundle|vendor|vendors|runtime|polyfill|polyfills|framework|app|main|index)[-_.][A-Za-z0-9_-]{4,}\.(js|mjs|cjs)$|[\da-f]{8,}\.(js|mjs|cjs)$|[-_.][\da-f]{20,}\.(js|mjs|cjs)$|\/dist\/|\/build\/|\/.next\/|\/out\/|\/node_modules\/|\.webpack\.|\.vite\.|\.turbopack\./i,Fs=/^\?[\w~.-]+(?:=[^&#]*)?(?:&[\w~.-]+(?:=[^&#]*)?)*$/,Rs=/(^|@)\S+:\d+/,Os=/^\s*at .*(\S+:\d+|\(native\))/m,Ds=/^(eval@)?(\[native code\])?$/,js=(e,t)=>{{let n=e.split("\n"),r=[];for(let e of n)if(/^\s*at\s+/.test(e)){let t=Is(e,void 0)[0];t&&r.push(t)}else if(/^\s*in\s+/.test(e)){let t=e.replace(/^\s*in\s+/,"").replace(/\s*\(at .*\)$/,"");r.push({functionName:t,source:e})}else if(e.match(Rs)){let t=Ws(e,void 0)[0];t&&r.push(t)}return Ps(r,t)}},Ls=e=>{if(!e.includes(":"))return[e,void 0,void 0];let t=e.startsWith("(")&&/:\d+\)$/.test(e)?e.slice(1,-1):e,n=/(.+?)(?::(\d+))?(?::(\d+))?$/.exec(t);return n?[n[1],n[2]||void 0,n[3]||void 0]:[t,void 0,void 0]},Ps=(e,t)=>t&&null!=t.slice?Array.isArray(t.slice)?e.slice(t.slice[0],t.slice[1]):e.slice(0,t.slice):e,Is=(e,t)=>Ps(e.split("\n").filter(e=>!!e.match(Os)),t).map(e=>{let t=e;t.includes("(eval ")&&(t=t.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));let n=t.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),r=n.match(/ (\(.+\)$)/);n=r?n.replace(r[0],""):n;let o=Ls(r?r[1]:n);return{functionName:r&&n||void 0,fileName:["eval",""].includes(o[0])?void 0:o[0],lineNumber:o[1]?+o[1]:void 0,columnNumber:o[2]?+o[2]:void 0,source:t}}),Ws=(e,t)=>Ps(e.split("\n").filter(e=>!e.match(Ds)),t).map(e=>{let t=e;if(t.includes(" > eval")&&(t=t.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),!t.includes("@")&&!t.includes(":"))return{functionName:t};{let e=/(([^\n\r"\u2028\u2029]*".[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*(?:@[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*)*(?:[\n\r\u2028\u2029][^@]*)?)?[^@]*)@/,n=t.match(e),r=n&&n[1]?n[1]:void 0,o=Ls(t.replace(e,""));return{functionName:r,fileName:o[0],lineNumber:o[1]?+o[1]:void 0,columnNumber:o[2]?+o[2]:void 0,source:t}}}),Us="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Hs=new Uint8Array(64),Bs=new Uint8Array(128);for(let e=0;e>>=1,i&&(n=-2147483648|-n),t+n}function qs(e,t){return!(e.pos>=t)&&44!==e.peek()}var Gs=class{constructor(e){this.pos=0,this.buffer=e}next(){return this.buffer.charCodeAt(this.pos++)}peek(){return this.buffer.charCodeAt(this.pos)}indexOf(e){let{buffer:t,pos:n}=this,r=t.indexOf(e,n);return-1===r?t.length:r}};function Js(e){let{length:t}=e,n=new Gs(e),r=[],o=0,i=0,a=0,s=0,l=0;do{let e=n.indexOf(";"),t=[],c=!0,d=0;for(o=0;n.pos{if(n<0||n>=e.length)return null;let o=e[n];if(!o||0===o.length)return null;let i=null;for(let e of o){if(!(e[0]<=r))break;i=e}if(!i||i.length<4)return null;let[,a,s,l]=i;if(void 0===a||void 0===s||void 0===l)return null;let c=t[a];return c?{columnNumber:l,fileName:c,lineNumber:s+1}:null},ol=e=>{if(!e)return!1;let t=e.trim();if(!t)return!1;let n=t.match(Ks);if(!n)return!0;let r=n[0].toLowerCase();return"http:"===r||"https:"===r},il=async(e,t=fetch)=>{if(!ol(e))return null;let n;try{let r=await t(e);if(!r.ok)return null;n=await r.text()}catch{return null}if(!n)return null;let r=((e,t)=>{let n,r=t.split("\n");for(let e=r.length-1;e>=0&&!n;e--){let t=r[e].match(Qs);t&&(n=t[1]||t[2])}if(!n)return null;let o=Ks.test(n);if(!(Zs.test(n)||o||n.startsWith("/"))){let t=e.split("/");t[t.length-1]=n,n=t.join("/")}return n})(e,n);if(!r||!ol(r))return null;try{let e=await t(r);if(!e.ok)return null;let n=await e.json();return"sections"in n?(e=>{let t=e.sections.map(({map:e,offset:t})=>({map:{...e,mappings:Js(e.mappings)},offset:t})),n=new Set;for(let e of t)for(let t of e.map.sources)n.add(t);return{file:e.file,mappings:[],names:[],sections:t,sourceRoot:void 0,sources:Array.from(n),sourcesContent:void 0,version:3}})(n):(e=>({file:e.file,mappings:Js(e.mappings),names:e.names,sourceRoot:e.sourceRoot,sources:e.sources,sourcesContent:e.sourcesContent,version:3}))(n)}catch{return null}},al=async(e,t=!0,n)=>{if(t&&tl.has(e)){let t=tl.get(e);if(null==t)return null;if(!(e=>el&&e instanceof WeakRef)(t))return t;{let n=t.deref();if(n)return n;tl.delete(e)}}if(t&&nl.has(e))return nl.get(e);let r=il(e,n);t&&nl.set(e,r);let o=await r;return t&&nl.delete(e),t&&(null===o?tl.set(e,null):tl.set(e,el?new WeakRef(o):o)),o},sl=async(e,t=!0,n)=>await Promise.all(e.map(async e=>{if(!e.fileName)return e;let r=await al(e.fileName,t,n);if(!r||"number"!=typeof e.lineNumber||"number"!=typeof e.columnNumber)return e;let o=((e,t,n)=>{if(e.sections){let r=null;for(let o of e.sections){if(!(t>o.offset.line||t===o.offset.line&&n>=o.offset.column))break;r=o}if(!r)return null;let o=t-r.offset.line,i=t===r.offset.line?n-r.offset.column:n;return rl(r.map.mappings,r.map.sources,o,i)}return rl(e.mappings,e.sources,t-1,n)})(r,e.lineNumber,e.columnNumber);return o?{...e,source:o.fileName&&e.source?e.source.replace(e.fileName,o.fileName):e.source,fileName:o.fileName,lineNumber:o.lineNumber,columnNumber:o.columnNumber,isSymbolicated:!0}:e})),ll=e=>{var t;return e._debugStack instanceof Error&&"string"==typeof(null==(t=e._debugStack)?void 0:t.stack)},cl=e=>{for(let t of gs){let n=t.currentDispatcherRef;n&&"object"==typeof n&&("H"in n?n.H=e:n.current=e)}},dl=e=>`\n in ${e}`,ul=(e,t)=>{let n=dl(e);return t&&(n+=` (at ${t})`),n},pl=!1,hl=(e,t)=>{var n;if(!e||pl)return"";let r=Error.prepareStackTrace;Error.prepareStackTrace=void 0,pl=!0;let o=(()=>{let e=bs();for(let t of[...Array.from(gs),...Array.from(e.renderers.values())]){let e=t.currentDispatcherRef;if(e&&"object"==typeof e)return"H"in e?e.H:e.current}return null})();cl(null);let i=console.error,a=console.warn;console.error=()=>{},console.warn=()=>{};try{let r={DetermineComponentFrameRoot(){let n;try{if(t){let t=function(){throw Error()};if(Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),"object"==typeof Reflect&&Reflect.construct){try{Reflect.construct(t,[])}catch(e){n=e}Reflect.construct(e,[],t)}else{try{t.call()}catch(e){n=e}e.call(t.prototype)}}else{try{throw Error()}catch(e){n=e}let t=e();t&&"function"==typeof t.catch&&t.catch(()=>{})}}catch(e){if(e instanceof Error&&n instanceof Error&&"string"==typeof e.stack)return[e.stack,n.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot",(null==(n=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,"name"))?void 0:n.configurable)&&Object.defineProperty(r.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});let[o,i]=r.DetermineComponentFrameRoot();if(o&&i){let t=o.split("\n"),n=i.split("\n"),r=0,a=0;for(;r=1&&a>=0&&t[r]!==n[a];)a--;for(;r>=1&&a>=0;r--,a--)if(t[r]!==n[a]){if(1!==r||1!==a)do{if(r--,a--,a<0||t[r]!==n[a]){let n=`\n${t[r].replace(" at new "," at ")}`,o=Ss(e);return o&&n.includes("")&&(n=n.replace("",o)),n}}while(r>=1&&a>=0);break}}}finally{pl=!1,Error.prepareStackTrace=r,cl(o),console.error=i,console.warn=a}let s=e?Ss(e):"";return s?dl(s):""},ml=(e,t)=>{let n="";switch(e.tag){case 28:n=dl("Activity");break;case 1:n=hl(e.type,!0);break;case 11:n=hl(e.type.render,!1);break;case 0:case 15:n=hl(e.type,!1);break;case 5:case 26:case 27:n=dl(e.type);break;case 16:n=dl("Lazy");break;case 13:n=e.child!==t&&null!==t?dl("Suspense Fallback"):dl("Suspense");break;case 19:n=dl("SuspenseList");break;case 30:n=dl("ViewTransition");break;default:return""}return n},fl=e=>{let t=Error.prepareStackTrace;Error.prepareStackTrace=void 0;let n=e;if(!n)return"";Error.prepareStackTrace=t,n.startsWith("Error: react-stack-top-frame\n")&&(n=n.slice(29));let r=n.indexOf("\n");return-1!==r&&(n=n.slice(r+1)),r=Math.max(n.indexOf("react_stack_bottom_frame"),n.indexOf("react-stack-bottom-frame")),-1!==r&&(r=n.lastIndexOf("\n",r)),-1===r?"":(n=n.slice(0,r),n)},gl=e=>{var t;return!(!(null==(t=e.fileName)?void 0:t.startsWith("rsc://"))||!e.functionName)},vl=(e,t)=>e.fileName===t.fileName&&e.lineNumber===t.lineNumber&&e.columnNumber===t.columnNumber,wl=async(e,t=!0,n)=>{let r=(e=>{let t=[];return ys(e,e=>{var n;if(!ll(e))return;let r="string"==typeof e.type?e.type:Ss(e.type)||"";t.push({componentName:r,stackFrames:js(fl(null==(n=e._debugStack)?void 0:n.stack))})},!0),t})(e),o=js((e=>{try{let t="",n=e,r=null;do{t+=ml(n,r);let e=n._debugInfo;if(e&&Array.isArray(e))for(let n=e.length-1;n>=0;n--){let r=e[n];"string"==typeof r.name&&(t+=ul(r.name,r.env))}r=n,n=n.return}while(n);return t}catch(e){return e instanceof Error?`\nError generating stack: ${e.message}\n${e.stack}`:""}})(e)),i=(e=>{var t;let n=new Map;for(let r of e)for(let e of r.stackFrames){if(!gl(e))continue;let r=e.functionName,o=null!=(t=n.get(r))?t:[];o.some(t=>vl(t,e))||(o.push(e),n.set(r,o))}return n})(r),a=new Map;return sl(o.map(e=>{var t,n;return null!=(n=null==(t=e.source)?void 0:t.includes("(at Server)"))&&n?((e,t,n)=>{var r,o;if(!e.functionName)return{...e,isServer:!0};let i=t.get(e.functionName);if(!i||0===i.length)return{...e,isServer:!0};let a=null!=(r=n.get(e.functionName))?r:0,s=i[a%i.length];return n.set(e.functionName,a+1),{...e,isServer:!0,fileName:s.fileName,lineNumber:s.lineNumber,columnNumber:s.columnNumber,source:null==(o=e.source)?void 0:o.replace("(at Server)",`(${s.fileName}:${s.lineNumber}:${s.columnNumber})`)}})(e,i,a):e}).filter((e,t,n)=>{if(0===t)return!0;let r=n[t-1];return e.functionName!==r.functionName}),t,n)},bl=e=>e.split("/").filter(Boolean).length,xl=e=>{let t=e.indexOf("/",1);if(-1===t||1!==bl(e.slice(0,t)))return e;let n=e.slice(t);if(!Ms.test(n)||bl(n)<2)return e;let r=(e=>{var t;return null!=(t=e.split("/").filter(Boolean)[0])?t:null})(n);return!r||r.startsWith("@")||r.length>4?e:n},yl=e=>{if(!e||As.some(t=>t===e))return"";let t=e,n=t.startsWith("http://")||t.startsWith("https://");if(n)try{t=new URL(t).pathname}catch{}if(n&&(t=xl(t)),t.startsWith("about://React/")){let e=t.slice(14),n=e.indexOf("/"),r=e.indexOf(":");t=-1!==n&&(-1===r||n{let t=yl(e);return!(!t||!Ms.test(t)||$s.test(t))},_l=Symbol.for("react.context"),Nl=Error("Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render."),Sl=e=>e._currentValue;typeof Proxy>"u"||new Proxy({readContext:Sl,use:e=>{if("object"==typeof e&&e){let t=e;if("function"==typeof t.then){let e=t;switch(e.status){case"fulfilled":return e.value,e.value;case"rejected":throw e.reason}throw Nl}if(t.$$typeof===_l&&"_currentValue"in t){let e=t,n=Sl(e);return e.displayName,n}}throw Error("An unsupported type was passed to use(): "+String(e))},useCallback:e=>e,useContext:e=>{let t=Sl(e);return e.displayName,t},useEffect:e=>{},useImperativeHandle:e=>{"object"==typeof e&&e&&"current"in e&&e.current},useLayoutEffect:e=>{},useInsertionEffect:e=>{},useMemo:e=>e(),useReducer:(e,t,n)=>[void 0===n?t:n(t),()=>{}],useRef:e=>{let t={current:e};return t},useState:e=>["function"==typeof e?e():e,()=>{}],useDebugValue:(e,t)=>{"function"==typeof t&&t(e)},useDeferredValue:e=>e,useTransition:()=>[false,()=>{}],useSyncExternalStore:(e,t)=>t(),useId:()=>"",useHostTransitionStatus:()=>Sl({_currentValue:null}),useFormState:(e,t)=>{let{value:n}=((e,t)=>{let n;return n=t,{value:n,error:null}})(0,t);return[n,()=>{},!1]},useActionState:(e,t)=>{let{value:n}=((e,t)=>{let n;return n=t,{value:n,error:null}})(0,t);return[n,()=>{},!1]},useOptimistic:e=>[e,()=>{}],useMemoCache:e=>[],useCacheRefresh:()=>()=>{},useEffectEvent:e=>e},{get(e,t){if(Object.prototype.hasOwnProperty.call(e,t))return e[t];let n=Error("Missing method in Dispatcher: "+t);throw n.name="ReactDebugToolsUnsupportedHookError",n}}),(()=>{try{typeof window<"u"&&((null==(e=window.document)?void 0:e.createElement)||"ReactNative"===(null==(t=window.navigator)?void 0:t.product))&&bs()}catch{}var e,t})();var Cl,Tl,El=(e,t)=>e.length>t?`${e.slice(0,t)}...`:e,zl=/^(?:\.\/)?\/?\([a-z][a-z0-9-]*\)\//,Al=e=>{let t=yl(e);return t=t.replace(zl,""),t.startsWith("./")&&(t=t.slice(2)),t},Ml=e=>e.startsWith("data-react-grab-"),$l=new Set(["_","$","motion.","styled.","chakra.","ark.","Primitive.","Slot."]),Fl=new Set(["InnerLayoutRouter","RedirectErrorBoundary","RedirectBoundary","HTTPAccessFallbackErrorBoundary","HTTPAccessFallbackBoundary","LoadingBoundary","ErrorBoundary","InnerScrollAndFocusHandler","ScrollAndFocusHandler","RenderFromTemplateContext","OuterLayoutRouter","body","html","DevRootHTTPAccessFallbackBoundary","AppDevOverlayErrorBoundary","AppDevOverlay","HotReload","Router","ErrorBoundaryHandler","AppRouter","ServerRoot","SegmentStateProvider","RootErrorBoundary","LoadableComponent","MotionDOMComponent"]),Rl=new Set(["Suspense","Fragment","StrictMode","Profiler","SuspenseList"]),Ol=e=>(null!=Tl||(Tl=typeof document<"u"&&!(!document.getElementById("__NEXT_DATA__")&&!document.querySelector("nextjs-portal"))),Tl),Dl=e=>{if(Fl.has(e)||Rl.has(e))return!0;for(let t of $l)if(e.startsWith(t))return!0;return!1},jl=e=>!(!e||Dl(e)||"SlotClone"===e||"Slot"===e),Ll=e=>!(e.length<=1||Dl(e)||e[0]!==e[0].toUpperCase()||e.endsWith("Provider")||e.endsWith("Context")),Pl=["about://React/","rsc://React/"],Il=e=>Pl.some(t=>e.startsWith(t)),Wl=e=>{for(let t of Pl){if(!e.startsWith(t))continue;let n=e.indexOf("/",t.length),r=e.lastIndexOf("?");if(n>-1&&r>-1)return decodeURI(e.slice(n+1,r))}return e},Ul=async e=>{var t,n,r,o,i,a;let s=[],l=[];for(let o=0;o",line1:null!=(n=i.lineNumber)?n:null,column1:null!=(r=i.columnNumber)?r:null,arguments:[]}))}if(0===l.length)return e;let c=new AbortController,d=setTimeout(()=>c.abort(),5e3);try{let t=await fetch(`${(()=>{var e;if(void 0!==Cl)return Cl;let t=null==(e=document.querySelector('script[src*="/_next/"]'))?void 0:e.src,n=t?new URL(t).pathname:"",r=n.indexOf("/_next/");return Cl=r>0?n.slice(0,r):""})()}/__nextjs_original-stack-frames`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({frames:l,isServer:!0,isEdgeServer:!1,isAppDirectory:!0}),signal:c.signal});if(!t.ok)return e;let n=await t.json(),r=[...e];for(let t=0;t{if(!t.some(e=>e.isServer&&!e.fileName&&e.functionName))return t;let n=(e=>{let t=new Map;return ys(e,e=>{if(!ll(e))return!1;let n=fl(e._debugStack.stack);if(!n)return!1;for(let e of js(n))!e.functionName||!e.fileName||Il(e.fileName)&&(t.has(e.functionName)||t.set(e.functionName,{...e,isServer:!0}));return!1},!0),t})(e);return 0===n.size?t:t.map(e=>{if(!e.isServer||e.fileName||!e.functionName)return e;let t=n.get(e.functionName);return t?{...e,fileName:t.fileName,lineNumber:t.lineNumber,columnNumber:t.columnNumber}:e})},Bl=e=>{if(!Cs())return e;let t=e;for(;t;){if(Ts(t))return t;t=t.parentElement}return e},Vl=new WeakMap,ql=e=>{if(!Cs())return Promise.resolve([]);let t=Bl(e),n=Vl.get(t);if(n)return n;let r=(async e=>{try{let t=Ts(e);if(!t)return null;let n=await wl(t);return Ol()?await Ul(Hl(t,n)):n}catch{return null}})(t);return Vl.set(t,r),r},Gl=e=>{if(!Cs())return null;let t=Ts(Bl(e));if(!t)return null;let n=t.return;for(;n;){if(xs(n)){let e=Ss(n.type);if(e&&jl(e))return e}n=n.return}return null},Jl=async(e,t={})=>{var n;let r=null!=(n=t.maxLines)?n:3,o=await ql(e);if(o&&(e=>!!e&&e.some(e=>!!(e.fileName&&kl(e.fileName)||e.isServer&&(!e.functionName||Ll(e.functionName)))))(o))return((e,t={})=>{let{maxLines:n=3}=t,r=Ol(),o=[];for(let t of e){if(o.length>=n)break;let e=t.fileName&&kl(t.fileName);if(!t.isServer||e||t.functionName&&!Ll(t.functionName)){if(e){let e="\n in ",n=t.functionName&&Ll(t.functionName);n&&(e+=`${t.functionName} (at `),e+=Al(t.fileName),r&&t.lineNumber&&(e+=`:${t.lineNumber}`,t.columnNumber&&(e+=`:${t.columnNumber}`)),n&&(e+=")"),o.push(e)}}else o.push(`\n in ${t.functionName||""} (at Server)`)}return o.join("")})(o,t);let i=((e,t)=>{if(!Cs())return[];let n=Ts(e);if(!n)return[];let r=[];return ys(n,e=>{if(r.length>=t)return!0;if(xs(e)){let t=Ss(e.type);t&&jl(t)&&r.push(t)}return!1},!0),r})(e,r);return i.length>0?i.map(e=>`\n in ${e}`).join(""):""},Yl=e=>El(e,15),Xl=e=>{var t,n,r,o,i,a;let s=rs(e),l=e instanceof HTMLElement?null!=(o=null!=(r=null==(t=e.innerText)?void 0:t.trim())?r:null==(n=e.textContent)?void 0:n.trim())?o:"":null!=(a=null==(i=e.textContent)?void 0:i.trim())?a:"",c="";for(let{name:t,value:n}of e.attributes)Ml(t)||(c+=` ${t}="${Yl(n)}"`);let d=[],u=[],p=!1,h=Array.from(e.childNodes);for(let e of h)e.nodeType!==Node.COMMENT_NODE&&(e.nodeType===Node.TEXT_NODE?e.textContent&&e.textContent.trim().length>0&&(p=!0):e instanceof Element&&(p?u.push(e):d.push(e)));let m=e=>0===e.length?"":e.length<=2?e.map(e=>`<${rs(e)} ...>`).join("\n "):`(${e.length} elements)`,f="",g=m(d);g&&(f+=`\n ${g}`),l.length>0&&(f+=`\n ${El(l,100)}`);let v=m(u);return v&&(f+=`\n ${v}`),f.length>0?`<${s}${c}>${f}\n`:`<${s}${c} />`},Kl=new Map,Zl=new WeakSet,Ql=new Map,ec=new Map;typeof window<"u"&&(window.requestAnimationFrame=e=>{if(!(e=>!!Zl.has(e))(e))return is(e);return is(t=>{e(t)})},window.cancelAnimationFrame=e=>{if(Kl.has(e))return void Kl.delete(e);let t=ec.get(e);if(void 0!==t)return as(t.nativeId),void ec.delete(e);let n=Ql.get(e);if(void 0!==n)return Kl.delete(n),void Ql.delete(e);as(e)});var tc,nc,rc=new Set(["role","name","aria-label","rel","href"]),oc=e=>{if(!/^[a-z\-]{3,}$/i.test(e))return!1;let t=e.split(/-|[A-Z]/);for(let e of t)if(e.length<=2||/[^aeiou]{4,}/i.test(e))return!1;return!0},ic=e=>{let t=e[0].name;for(let n=1;n ${t}`;return t},ac=e=>{let t=0;for(let n of e)t+=n.penalty;return t},sc=(e,t)=>ac(e)-ac(t),lc=(e,t)=>{let n=e.parentNode;if(!n)return;let r=n.firstChild;if(!r)return;let o=0;for(;r&&(r.nodeType===Node.ELEMENT_NODE&&(void 0===t||r.tagName.toLowerCase()===t)&&o++,r!==e);)r=r.nextSibling;return o},cc=(e,t)=>"html"===e?"html":`${e}:nth-child(${t})`,dc=(e,t)=>"html"===e?"html":`${e}:nth-of-type(${t})`,uc=(e,t)=>{let n=[],r=e.getAttribute("id"),o=e.tagName.toLowerCase();r&&oc(r)&&n.push({name:`#${CSS.escape(r)}`,penalty:0});for(let t of e.classList)oc(t)&&n.push({name:`.${CSS.escape(t)}`,penalty:1});for(let r of e.attributes)t(r.name,r.value)&&n.push({name:`[${CSS.escape(r.name)}="${CSS.escape(r.value)}"]`,penalty:2});n.push({name:o,penalty:5});let i=lc(e,o);void 0!==i&&n.push({name:dc(o,i),penalty:10});let a=lc(e);return void 0!==a&&n.push({name:cc(o,a),penalty:50}),n},pc=(e,t=1e4,n=[])=>{if(t<=0)return[];if(0===e.length)return[n];let r=[];for(let o of e[0]){let i=t-r.length;if(i<=0)break;r.push(...pc(e.slice(1),i,[...n,o]))}return r},hc=(e,t)=>1===t.querySelectorAll(ic(e)).length,mc=(e,t)=>{let n=e,r=[];for(;n&&n!==t;){let e=n.tagName.toLowerCase(),t=lc(n,e);if(void 0===t)return;r.push({name:dc(e,t),penalty:10}),n=n.parentElement}return hc(r,t)?r:void 0},fc=(e,t,n,r)=>{if(e.nodeType!==Node.ELEMENT_NODE)throw Error("Can't generate CSS selector for non-element node type.");if("html"===e.tagName.toLowerCase())return"html";let o,i=((e,t)=>{var n;let r=null==(n=t.getRootNode)?void 0:n.call(t);return r instanceof ShadowRoot?r:e.nodeType===Node.DOCUMENT_NODE?e:e.ownerDocument})(t,e),a=Date.now(),s=[],l=e,c=0;for(;l&&l!==i&&!o;)if(s.push(uc(l,r)),l=l.parentElement,c++,c>=3){let t=pc(s);t.sort(sc);for(let r of t){if(Date.now()-a>n){let t=mc(e,i);if(!t)throw Error(`Timeout: Can't find a unique selector after ${n}ms`);return ic(t)}if(hc(r,i)){o=r;break}}}if(!o&&c<3){let e=pc(s);e.sort(sc);for(let t of e){if(Date.now()-a>n)break;if(hc(t,i)){o=t;break}}}if(!o)throw Error("Selector was not found.");return ic(o)},gc=e=>typeof CSS<"u"&&"function"==typeof CSS.escape?CSS.escape(e):e.replace(/[^a-zA-Z0-9_-]/g,e=>`\\${e}`),vc=e=>{var t;return null!=(t=e.ownerDocument.body)?t:e.ownerDocument.documentElement},wc=new Set(["data-testid","data-test-id","data-test","data-cy","data-qa","aria-label","role","name","title","alt"]),bc=e=>e.length>0&&e.length<=120,xc=(e,t)=>{try{let n=e.ownerDocument.querySelectorAll(t);return 1===n.length&&n[0]===e}catch{return!1}},yc=(e,t=!0)=>{let n=(e=>{if(e instanceof HTMLElement&&e.id){let t=`#${gc(e.id)}`;if(xc(e,t))return t}for(let t of wc){let n=e.getAttribute(t);if(!n||!bc(n))continue;let r=`[${t}=${JSON.stringify(n)}]`;if(xc(e,r))return r;let o=`${e.tagName.toLowerCase()}${r}`;if(xc(e,o))return o}return null})(e);if(n)return n;if(t)try{let t=fc(e,vc(e),200,(e,t)=>((e,t)=>{let n=rc.has(e)||e.startsWith("data-")&&oc(e),r=oc(t)&&t.length<100||t.startsWith("#")&&oc(t.slice(1));return n&&r})(e,t)||wc.has(e)&&bc(t));if(t)return t}catch{}return(e=>{let t=[],n=vc(e),r=e;for(;r;){if(r instanceof HTMLElement&&r.id){t.unshift(`#${gc(r.id)}`);break}let e=r.parentElement;if(!e){t.unshift(r.tagName.toLowerCase());break}let o=Array.from(e.children).indexOf(r),i=o>=0?o+1:1;if(t.unshift(`${r.tagName.toLowerCase()}:nth-child(${i})`),e===n){t.unshift(n.tagName.toLowerCase());break}r=e}return t.join(" > ")})(e)},kc=new Map(["top","right","bottom","left"].flatMap(e=>[[`border-${e}-style`,e],[`border-${e}-color`,e]])),_c=null,Nc=new Map,Sc=e=>{let t=Nc.get(e);if(t)return t;let n=_c||((_c=document.createElement("iframe")).style.cssText="position:fixed;left:-9999px;width:0;height:0;border:none;visibility:hidden;",document.body.appendChild(_c),_c),r=n.contentDocument,o=r.createElement(e);r.body.appendChild(o);let i=n.contentWindow.getComputedStyle(o),a=new Map;for(let e of ns){let t=i.getPropertyValue(e);t&&a.set(e,t)}return o.remove(),Nc.set(e,a),a},Cc=(e,t)=>{let n=kc.get(e);if(!n)return!1;let r=t.getPropertyValue(`border-${n}-width`);return"0px"===r||"0"===r},Tc=e=>{var t;let n=Sc(e.tagName.toLowerCase()),r=getComputedStyle(e),o=[];for(let e of ns){let t=r.getPropertyValue(e);t&&t!==n.get(e)&&(Cc(e,r)||o.push(`${e}: ${t};`))}let i=null==(t=e.getAttribute("class"))?void 0:t.trim(),a=o.join("\n");return i?a?`className: ${i}\n\n${a}`:`className: ${i}`:a},Ec=async e=>{try{const t=await(async e=>{var t;let n=null!=(t=await ql(e))?t:[],r=await Jl(e);return{element:e,htmlPreview:Xl(e),stackString:r,stack:n,componentName:Gl(e),fiber:Ts(e),selector:yc(e),styles:Tc(e)}})(e),n=`${t.htmlPreview}${t.stackString}`;return!!n.trim()&&(await navigator.clipboard.writeText(n),!0)}catch{return!1}},zc=Et(()=>lo("absolute inset-0 flex items-center gap-x-2","translate-y-0","transition-transform duration-300",wo.value&&"-translate-y-[200%]")),Ac=()=>{const e=Xe(null),t=Xe(null),[n,r]=Ge(null);Jt(()=>{const e=Lu.inspectState.value;"focused"===e.kind&&r(e.fiber)}),Jt(()=>{const n=zo.value;ut(()=>{var r,o;if("focused"!==Lu.inspectState.value.kind)return;if(!e.current||!t.current)return;const{totalUpdates:i,currentIndex:a,updates:s,isVisible:l,windowOffset:c}=n,d=Math.max(0,i-1),u=l?`#${c+a} Re-render`:d>0?`×${d}`:"";let p;if(d>0&&a>=0&&a0?e<.1-Number.EPSILON?"< 0.1ms":`${Number(e.toFixed(1))}ms`:void 0}e.current.dataset.text=u?` • ${u}`:"",t.current.dataset.text=p?` • ${p}`:""})});const o=Ke(()=>{if(!n)return null;const{name:e,wrappers:t,wrapperTypes:r}=mo(n),o=t.length?`${t.join("(")}(${e})${")".repeat(t.length)}`:null!=e?e:"",i=r[0];return Rn("span",{title:o,className:"flex items-center gap-x-1",children:[null!=e?e:"Unknown",Rn("span",{title:null==i?void 0:i.title,className:"flex items-center gap-x-1 text-[10px] text-purple-400",children:!!i&&Rn(me,{children:[Rn("span",{className:lo("rounded py-[1px] px-1","truncate",i.compiler&&"bg-purple-800 text-neutral-400",!i.compiler&&"bg-neutral-700 text-neutral-300","memo"===i.type&&"bg-[#5f3f9a] text-white"),children:i.type},i.type),i.compiler&&Rn("span",{className:"text-yellow-300",children:"✨"})]})}),r.length>1&&Rn("span",{className:"text-[10px] text-neutral-400",children:["×",r.length-1]})]})},[n]);return Rn("div",{className:zc,children:[o,Rn("div",{className:"flex items-center gap-x-2 mr-auto text-xs text-[#888]",children:[Rn("span",{ref:e,className:"with-data-text cursor-pointer !overflow-visible",title:"Click to toggle between rerenders and total renders"}),Rn("span",{ref:t,className:"with-data-text !overflow-visible"})]})]})},Mc=()=>{const e=((e,t,n=t)=>{const[r,o]=Ge(e);return Je(()=>{if(e===r)return;const i=setTimeout(()=>o(e),e?t:n);return()=>clearTimeout(i)},[e,t,n]),r})("focused"===Lu.inspectState.value.kind,150,0),t=Wt(!1),n=()=>{_o.value={view:"none"},Lu.inspectState.value={kind:"inspect-off"}},r=async()=>{const e=Lu.inspectState.value;if("focused"!==e.kind||!e.focusedDomElement)return;await Ec(e.focusedDomElement)&&(t.value=!0,setTimeout(()=>{t.value=!1,n()},600))},o=Xe(r);o.current=r,Je(()=>{const e=e=>{const t=Lu.inspectState.value;"focused"===t.kind&&t.focusedDomElement&&("undefined"!=typeof window&&Boolean(window.__REACT_GRAB__)||(e.metaKey||e.ctrlKey)&&(e.shiftKey||e.altKey||"c"!==e.key&&"KeyC"!==e.code||(()=>{const e=document.activeElement;if(!e)return!1;const t=e.tagName;return"INPUT"===t||"TEXTAREA"===t||"SELECT"===t||!!(e instanceof HTMLElement&&e.isContentEditable)})()||(()=>{var e;const t=null==(e=window.getSelection)?void 0:e.call(window);return Boolean(t&&t.toString().length>0)})()||(e.preventDefault(),e.stopImmediatePropagation(),o.current())))};return document.addEventListener("keydown",e,{capture:!0}),()=>{document.removeEventListener("keydown",e,{capture:!0})}},[]);if("notifications"===_o.value.view)return;const i="focused"===Lu.inspectState.value.kind,a=(()=>{if("undefined"==typeof navigator)return!1;const e=navigator.platform||"";return/Mac|iPhone|iPad|iPod/i.test(e||navigator.userAgent)})()?"⌘C":"Ctrl+C";return Rn("div",{className:"react-scan-header",children:[Rn("div",{className:"relative flex-1 h-full",children:Rn("div",{className:lo("react-scan-header-item is-visible",!e&&"!duration-0"),children:Rn(Ac,{})})}),i&&Rn("button",{type:"button",title:`Copy element (${a})`,className:"react-scan-close-button",onClick:r,children:Rn(On,{name:t.value?"icon-check":"icon-copy",className:lo(t.value&&"text-green-500")})}),Rn("button",{type:"button",title:"Close",className:"react-scan-close-button",onClick:n,children:Rn(On,{name:"icon-close"})})]})},$c=({className:e,...t})=>Rn("div",{className:lo("react-scan-toggle",e),children:[Rn("input",{type:"checkbox",...t}),Rn("div",{})]}),Fc=({fps:e})=>{return Rn("div",{className:lo("flex items-center gap-x-1 px-2 w-full","h-6","rounded-md","font-mono leading-none","bg-[#141414]","ring-1 ring-white/[0.08]"),children:[Rn("div",{style:{color:(t=e,t<30?"#EF4444":t<50?"#F59E0B":"rgb(214,132,245)")},className:"text-sm font-semibold tracking-wide transition-colors ease-in-out w-full flex justify-center items-center",children:e}),Rn("span",{className:"text-white/30 text-[11px] font-medium tracking-wide ml-auto min-w-fit",children:"FPS"})]});var t},Rc=()=>{const[e,t]=Ge(null);return Je(()=>{const e=setInterval(()=>{t(ra())},200);return()=>clearInterval(e)},[]),Rn("div",{className:lo("flex items-center justify-end gap-x-2 px-1 ml-1 w-[72px]","whitespace-nowrap text-sm text-white"),children:null===e?Rn(me,{children:"️"}):Rn(Fc,{fps:e})})},Oc=e=>e(),Dc=class e extends Array{constructor(e=25){super(),n(this,"capacity",e)}push(...e){const t=super.push(...e);for(;this.length>this.capacity;)this.shift();return t}static fromArray(t,n){const r=new e(n);return r.push(...t),r}},jc=new class{constructor(e){n(this,"subscribers",new Set),n(this,"currentValue"),this.currentValue=e}subscribe(e){return this.subscribers.add(e),e(this.currentValue),()=>{this.subscribers.delete(e)}}setState(e){this.currentValue=e,this.subscribers.forEach(t=>t(e))}getCurrentState(){return this.currentValue}}(new Dc(150)),Lc=50,Pc=new class{constructor(){n(this,"channels",{})}publish(e,t,n=!0){const r=this.channels[t];if(!r){if(!n)return;return this.channels[t]={callbacks:new Dc(Lc),state:new Dc(Lc)},void this.channels[t].state.push(e)}r.state.push(e),r.callbacks.forEach(t=>t(e))}getAvailableChannels(){return Dc.fromArray(Object.keys(this.channels),Lc)}subscribe(e,t,n=!1){const r=()=>(n||this.channels[e].state.forEach(e=>{t(e)}),()=>{const n=this.channels[e].callbacks.filter(e=>e!==t);this.channels[e].callbacks=Dc.fromArray(n,Lc)}),o=this.channels[e];return o?(o.callbacks.push(t),r()):(this.channels[e]={callbacks:new Dc(Lc),state:new Dc(Lc)},this.channels[e].callbacks.push(t),r())}updateChannelState(e,t,n=!0){const r=this.channels[e];if(!r){if(!n)return;const r=new Dc(Lc),o={callbacks:new Dc(Lc),state:r};return this.channels[e]=o,void(o.state=t(r))}r.state=t(r.state)}getChannelState(e){var t;return null!=(t=this.channels[e].state)?t:new Dc(Lc)}},Ic={skipProviders:!0,skipHocs:!0,skipContainers:!0,skipMinified:!0,skipUtilities:!0,skipBoundaries:!0},Wc={providers:[/Provider$/,/^Provider$/,/^Context$/],hocs:[/^with[A-Z]/,/^forward(?:Ref)?$/i,/^Forward(?:Ref)?\(/],containers:[/^(?:App)?Container$/,/^Root$/,/^ReactDev/],utilities:[/^Fragment$/,/^Suspense$/,/^ErrorBoundary$/,/^Portal$/,/^Consumer$/,/^Layout$/,/^Router/,/^Hydration/],boundaries:[/^Boundary$/,/Boundary$/,/^Provider$/,/Provider$/]},Uc=(e,t=Ic)=>{const n=[];return t.skipProviders&&n.push(...Wc.providers),t.skipHocs&&n.push(...Wc.hocs),t.skipContainers&&n.push(...Wc.containers),t.skipUtilities&&n.push(...Wc.utilities),t.skipBoundaries&&n.push(...Wc.boundaries),!n.some(t=>t.test(e))},Hc=[/^[a-z]$/,/^[a-z][0-9]$/,/^_+$/,/^[A-Za-z][_$]$/,/^[a-z]{1,2}$/],Bc=e=>{var t,n;for(let t=0;te.length/2,i=/^[a-z]+$/.test(e),a=/[$_]{2,}/.test(e);return Number(r)+Number(o)+Number(i)+Number(a)>=2},Vc=e=>{const t=J(e);return t?t.replace(/^(?:Memo|Forward(?:Ref)?|With.*?)\((?.*?)\)$/,"$"):""},qc="never-hidden",Gc=null,Jc=e=>{(()=>{null==tc||tc();const e=()=>{document.hidden&&(qc=Date.now())};document.addEventListener("visibilitychange",e),tc=()=>{document.removeEventListener("visibilitychange",e)}})();const t=new Map,n=new Map,r=r=>{if(!r.interactionId)return;if(r.interactionId&&r.target&&!n.has(r.interactionId)&&n.set(r.interactionId,r.target),r.target){let e=r.target;for(;e;){if("react-scan-toolbar-root"===e.id||"react-scan-root"===e.id)return;e=e.parentElement}}const o=t.get(r.interactionId);if(o)r.duration>o.latency?(o.entries=[r],o.latency=r.duration):r.duration===o.latency&&r.startTime===o.entries[0].startTime&&o.entries.push(r);else{const n=(i=r.name,["pointerup","click"].includes(i)?"pointer":(i.includes("key"),["keydown","keyup"].includes(i)?"keyboard":null));if(!n)return;const o={id:r.interactionId,latency:r.duration,entries:[r],target:r.target,type:n,startTime:r.startTime,endTime:Date.now(),processingStart:r.processingStart,processingEnd:r.processingEnd,duration:r.duration,inputDelay:r.processingStart-r.startTime,processingDuration:r.processingEnd-r.processingStart,presentationDelay:r.duration-(r.processingEnd-r.startTime),timestamp:Date.now(),timeSinceTabInactive:"never-hidden"===qc?"never-hidden":Date.now()-qc,visibilityState:document.visibilityState,timeOrigin:performance.timeOrigin,referrer:document.referrer};t.set(o.id,o),Gc||(Gc=requestAnimationFrame(()=>{requestAnimationFrame(()=>{e(t.get(o.id)),Gc=null})}))}var i},o=new PerformanceObserver(e=>{const t=e.getEntries();for(let e=0,n=t.length;eo.disconnect()},Yc=new Dc(25),Xc=e=>Pc.subscribe("recording",t=>{const n="auto-complete-race"===t.kind?Yc.find(e=>e.interactionUUID===t.interactionUUID):((e,t)=>{let n=null;for(const r of t){if(r.type!==e.type)continue;if(null===n){n=r;continue}const t=(e,t)=>Math.abs(e.startDateTime)-(t.startTime+t.timeOrigin);t(r,e){var t;const n=gi(e);if(!n)return;let r=n?J(null==n?void 0:n.type):"N/A";if(r||(r=null!=(t=((e,t=()=>!0)=>{let n=e;for(;n;){const e=J(n.type);if(e&&t(e))return e;n=n.return}return null})(n,e=>e.length>2))?t:"N/A"),!r)return;return{componentPath:((e,t=Ic)=>{if(!e)return[];if(!J(e.type))return[];const n=new Array;let r=e;for(;r.return;){const e=Vc(r.type);e&&!Bc(e)&&Uc(e,t)&&e.toLowerCase()!==e&&n.push(e),r=r.return}const o=new Array(n.length);for(let e=0;e{let n=null;const r=t=>{switch(e){case"pointer":return"start"===t.phase?"pointerup":t.target instanceof HTMLInputElement||t.target instanceof HTMLSelectElement?"change":"click";case"keyboard":return"start"===t.phase?"keydown":"change"}},o={current:{kind:"uninitialized-stage",interactionUUID:tn(),stageStart:Date.now(),interactionType:e}},i=n=>{var i,s;if(n.composedPath().some(e=>e instanceof Element&&"react-scan-toolbar-root"===e.id))return;if(Date.now()-o.current.stageStart>2e3&&(o.current={kind:"uninitialized-stage",interactionUUID:tn(),stageStart:Date.now(),interactionType:e}),"uninitialized-stage"!==o.current.kind)return;const l=performance.now();null==(i=null==t?void 0:t.onStart)||i.call(t,o.current.interactionUUID);const c=Kc(n.target);if(!c)return void(null==(s=null==t?void 0:t.onError)||s.call(t,o.current.interactionUUID));const d={},u=td(d);o.current={...o.current,interactionType:e,blockingTimeStart:Date.now(),childrenTree:c.childrenTree,componentName:c.componentName,componentPath:c.componentPath,fiberRenders:d,kind:"interaction-start",interactionStartDetail:l,stopListeningForRenders:u};const p=r({phase:"end",target:n.target});document.addEventListener(p,a,{once:!0}),requestAnimationFrame(()=>{document.removeEventListener(p,a)})};document.addEventListener(r({phase:"start"}),i,{capture:!0});const a=(r,i,a)=>{var s;if("interaction-start"!==o.current.kind&&i===n)return"pointer"===e&&r.target instanceof HTMLSelectElement||null==(s=null==t?void 0:t.onError)||s.call(t,o.current.interactionUUID),void(o.current={kind:"uninitialized-stage",interactionUUID:tn(),stageStart:Date.now(),interactionType:e});n=i,(({onMicroTask:e,onRAF:t,onTimeout:n,abort:r})=>{queueMicrotask(()=>{!0!==(null==r?void 0:r())&&e()&&requestAnimationFrame(()=>{!0!==(null==r?void 0:r())&&t()&&setTimeout(()=>{!0!==(null==r?void 0:r())&&n()},0)})})})({abort:a,onMicroTask:()=>"uninitialized-stage"!==o.current.kind&&(o.current={...o.current,kind:"js-end-stage",jsEndDetail:performance.now()},!0),onRAF:()=>{var n;return"js-end-stage"!==o.current.kind&&"raf-stage"!==o.current.kind?(null==(n=null==t?void 0:t.onError)||n.call(t,o.current.interactionUUID),o.current={kind:"uninitialized-stage",interactionUUID:tn(),stageStart:Date.now(),interactionType:e},!1):(o.current={...o.current,kind:"raf-stage",rafStart:performance.now()},!0)},onTimeout:()=>{var n;if("raf-stage"!==o.current.kind)return null==(n=null==t?void 0:t.onError)||n.call(t,o.current.interactionUUID),void(o.current={kind:"uninitialized-stage",interactionUUID:tn(),stageStart:Date.now(),interactionType:e});const r=Date.now(),i=Object.freeze({...o.current,kind:"timeout-stage",blockingTimeEnd:r,commitEnd:performance.now()});o.current={kind:"uninitialized-stage",interactionUUID:tn(),stageStart:r,interactionType:e};let a=!1;const s=e=>{var n;a=!0;const r="auto-complete-race"===e.kind?e.detailedTiming.commitEnd-e.detailedTiming.interactionStartDetail:e.entry.latency,o={detailedTiming:i,latency:r,completedAt:Date.now(),flushNeeded:!0};null==(n=null==t?void 0:t.onComplete)||n.call(t,i.interactionUUID,o,e);const s=Yc.filter(e=>e.interactionUUID!==i.interactionUUID);return Yc=Dc.fromArray(s,25),o},l={completeInteraction:s,endDateTime:Date.now(),startDateTime:i.blockingTimeStart,type:e,interactionUUID:i.interactionUUID};if(Yc.push(l),ed())setTimeout(()=>{if(a)return;s({kind:"auto-complete-race",detailedTiming:i,interactionUUID:i.interactionUUID});const e=Yc.filter(e=>e.interactionUUID!==i.interactionUUID);Yc=Dc.fromArray(e,25)},1e3);else{const e=Yc.filter(e=>e.interactionUUID!==i.interactionUUID);Yc=Dc.fromArray(e,25),s({kind:"auto-complete-race",detailedTiming:i,interactionUUID:i.interactionUUID})}}})},s=e=>{const t=tn();a(e,t,()=>t!==n)};return"keyboard"===e&&document.addEventListener("keypress",s),()=>{document.removeEventListener(r({phase:"start"}),i,{capture:!0}),document.removeEventListener("keypress",s)}},Qc=e=>{var t;return null==(t=k(e,e=>{if(v(e))return!0}))?void 0:t.stateNode},ed=()=>"PerformanceEventTiming"in globalThis,td=e=>{const t=t=>{var n,r,o,i,a,s,l;const c=J(t.type);if(!c)return;const d=e[c];if(!d){const r=new Set,o=t.return&&bi(t.return),i=o&&J(o[0]);i&&r.add(i);const{selfTime:a,totalTime:s}=V(t),l=Xi(t),d={current:[],changes:new Set,changesCounts:new Map},u={fiberProps:l.fiberProps||d,fiberState:l.fiberState||d,fiberContext:l.fiberContext||d};return void(e[c]={renderCount:1,hasMemoCache:q(t),wasFiberRenderMount:rd(t),parents:r,selfTime:a,totalTime:s,nodeInfo:[{element:Qc(t),name:null!=(n=J(t.type))?n:"Unknown",selfTime:V(t).selfTime}],changes:u})}if(null==(o=null==(r=bi(t))?void 0:r[0])?void 0:o.type){const e=t.return&&bi(t.return),n=e&&J(e[0]);n&&d.parents.add(n)}const{selfTime:u,totalTime:p}=V(t),h=Xi(t);if(!h)return;const m={current:[],changes:new Set,changesCounts:new Map};d.wasFiberRenderMount=d.wasFiberRenderMount||rd(t),d.hasMemoCache=d.hasMemoCache||q(t),d.changes={fiberProps:nd((null==(i=d.changes)?void 0:i.fiberProps)||m,h.fiberProps||m),fiberState:nd((null==(a=d.changes)?void 0:a.fiberState)||m,h.fiberState||m),fiberContext:nd((null==(s=d.changes)?void 0:s.fiberContext)||m,h.fiberContext||m)},d.renderCount+=1,d.selfTime+=u,d.totalTime+=p,d.nodeInfo.push({element:Qc(t),name:null!=(l=J(t.type))?l:"Unknown",selfTime:V(t).selfTime})};return Lu.interactionListeningForRenders=t,()=>{Lu.interactionListeningForRenders===t&&(Lu.interactionListeningForRenders=null)}},nd=(e,t)=>{const n={current:[...e.current],changes:new Set,changesCounts:new Map};for(const e of t.current)n.current.some(t=>t.name===e.name)||n.current.push(e);for(const r of t.changes)if("string"==typeof r||"number"==typeof r){n.changes.add(r);const o=e.changesCounts.get(r)||0,i=t.changesCounts.get(r)||0;n.changesCounts.set(r,o+i)}return n},rd=e=>{if(!e.alternate)return!0;const t=e.alternate,n=t&&null!=t.memoizedState&&null!=t.memoizedState.element&&!0!==t.memoizedState.isDehydrated,r=null!=e.memoizedState&&null!=e.memoizedState.element&&!0!==e.memoizedState.isDehydrated;return!n&&r},od=null,id=(e=>{let t;const n=new Set,r=(e,r)=>{const o="function"==typeof e?e(t):e;if(!Object.is(o,t)){const e=t;t=(null!=r?r:"object"!=typeof o||null===o)?o:Object.assign({},t,o),n.forEach(n=>n(t,e))}},o=()=>t,i={setState:r,getState:o,getInitialState:()=>a,subscribe:(e,r)=>{let o,i;r?(o=e,i=r):i=e;let a=o?o(t):void 0;const s=(e,t)=>{if(o){const n=o(e),r=o(t);Object.is(a,n)||(a=n,i(n,r))}else i(e,t)};return n.add(s),()=>n.delete(s)}},a=t=e(r,o,i);return i})((e,t)=>{const n=new Set;return{state:{events:new Dc(200)},actions:{addEvent:r=>{n.forEach(e=>e(r));const o=[...t().state.events,r],i=new Set;o.forEach(e=>{"interaction"!==e.kind&&((e,t)=>{const n=o.find(t=>{if("long-render"!==t.kind&&t.id!==e.id)return e.data.startAt<=t.data.startAt&&e.data.endAt<=t.data.endAt&&e.data.endAt>=t.data.startAt||t.data.startAt<=e.data.startAt&&t.data.endAt>=e.data.startAt||e.data.startAt<=t.data.startAt&&e.data.endAt>=t.data.endAt||void 0});n&&t(n)})(e,()=>{i.add(e.id)})});const a=o.filter(e=>!i.has(e.id));e(()=>({state:{events:Dc.fromArray(a,200)}}))},addListener:e=>(n.add(e),()=>{n.delete(e)}),clear:()=>{e({state:{events:new Dc(200)}})}}}}),ad=()=>{return e=id.subscribe,t=id.getState,n=t(),r=Ge({t:{__:n,u:t}}),o=r[0].t,i=r[1],Ye(function(){o.__=n,o.u=t,an(o)&&i({t:o})},[e,n,t]),Je(function(){return an(o)&&i({t:o}),e(function(){an(o)&&i({t:o})})},[e]),n;var e,t,n,r,o,i},sd=null,ld=null,cd=null,dd=[];var ud=()=>{const e=Jc(e=>{Pc.publish({kind:"entry-received",entry:e},"recording")}),t=(()=>{const e=e=>{nc=e.composedPath().map(e=>e.id).filter(Boolean).includes("react-scan-toolbar")};return document.addEventListener("mouseover",e),cd=e,()=>{cd&&document.removeEventListener("mouseover",cd)}})(),n=(()=>{const e=()=>{sd=performance.now(),ld=performance.timeOrigin};return document.addEventListener("visibilitychange",e),()=>{document.removeEventListener("visibilitychange",e)}})(),r=function(){let e,t;const n=function n(){let r=null;od=null,r=td(od={});const o=performance.timeOrigin,i=performance.now();return e=requestAnimationFrame(()=>{t=setTimeout(()=>{const e=performance.now(),t=e-i,a=performance.timeOrigin;dd.push(e+a);const s=dd.filter(t=>e+a-t<=1e3),l=s.length;dd=s;const c=null!==nc&&nc;if(t>150&&!(null!==sd&&null!==ld&&e+a-(ld+sd)<100)&&"visible"===document.visibilityState&&!c){const n=a+e,r=i+o;id.getState().actions.addEvent({kind:"long-render",id:tn(),data:{endAt:n,startAt:r,meta:{fiberRenders:od,latency:t,fps:l}}})}sd=null,ld=null,null==r||r(),n()},0)}),r}();return()=>{n(),cancelAnimationFrame(e),clearTimeout(t)}}(),o=async(e,t,n)=>{id.getState().actions.addEvent({kind:"interaction",id:tn(),data:{startAt:t.detailedTiming.blockingTimeStart,endAt:performance.now()+performance.timeOrigin,meta:{...t,kind:n.kind}}});const r=Pc.getChannelState("recording");t.detailedTiming.stopListeningForRenders(),r.length&&Pc.updateChannelState("recording",()=>new Dc(Lc))},i=Zc("pointer",{onComplete:o}),a=Zc("keyboard",{onComplete:o}),s=Xc(e=>{jc.setState(Dc.fromArray(jc.getCurrentState().concat(e),150))});return()=>{t(),n(),r(),e(),i(),s(),a()}},pd=e=>{var t;const n=e.filter(e=>e.length>2);return 0===n.length?null!=(t=e.at(-1))?t:"Unknown":n.at(-1)},hd=e=>{switch(e.kind){case"interaction":{const{renderTime:t,otherJSTime:n,framePreparation:r,frameConstruction:o,frameDraw:i}=e;return t+n+r+o+(null!=i?i:0)}case"dropped-frames":return e.otherTime+e.renderTime}},md=e=>{const t=hd(e.timing);switch(e.kind){case"interaction":return t<200?"low":t<500?"needs-improvement":"high";case"dropped-frames":return t<50?"low":t<150?"needs-improvement":"high"}},fd=()=>Qe(gd),gd=De(null),vd=({size:e=24,className:t})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:e,height:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:lo(["lucide lucide-chevron-right",t]),children:Rn("path",{d:"m9 18 6-6-6-6"})}),wd=({className:e="",size:t=24,events:n=[]})=>{const r=n.includes(!0),o=n.filter(e=>e).length,i=o>99?">99":o,a=r?Math.max(.6*t,14):Math.max(.4*t,6);return Rn("div",{className:"relative",children:[Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:`lucide lucide-bell ${e}`,children:[Rn("path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}),Rn("path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"})]}),n.length>0&&o>0&&Pu.options.value.showNotificationCount&&Rn("div",{className:lo(["absolute",r?"-top-2.5 -right-2.5":"-top-1 -right-1","rounded-full","flex items-center justify-center","text-[8px] font-medium text-white","aspect-square",r?"bg-red-500/90":"bg-purple-500/90"]),style:{width:`${a}px`,height:`${a}px`,padding:r?"0.5px":"0"},children:r&&i})]})},bd=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:e,children:[Rn("path",{d:"M18 6 6 18"}),Rn("path",{d:"m6 6 12 12"})]}),xd=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:e,children:[Rn("path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}),Rn("path",{d:"M16 9a5 5 0 0 1 0 6"}),Rn("path",{d:"M19.364 18.364a9 9 0 0 0 0-12.728"})]}),yd=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:e,children:[Rn("path",{d:"M16 9a5 5 0 0 1 .95 2.293"}),Rn("path",{d:"M19.364 5.636a9 9 0 0 1 1.889 9.96"}),Rn("path",{d:"m2 2 20 20"}),Rn("path",{d:"m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11"}),Rn("path",{d:"M9.828 4.172A.686.686 0 0 1 11 4.657v.686"})]}),kd=({size:e=24,className:t})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:e,height:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:lo(["lucide lucide-arrow-left",t]),children:[Rn("path",{d:"m12 19-7-7 7-7"}),Rn("path",{d:"M19 12H5"})]}),_d=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:e,children:[Rn("path",{d:"M14 4.1 12 6"}),Rn("path",{d:"m5.1 8-2.9-.8"}),Rn("path",{d:"m6 12-1.9 2"}),Rn("path",{d:"M7.2 2.2 8 5.1"}),Rn("path",{d:"M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z"})]}),Nd=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:e,children:[Rn("path",{d:"M10 8h.01"}),Rn("path",{d:"M12 12h.01"}),Rn("path",{d:"M14 8h.01"}),Rn("path",{d:"M16 12h.01"}),Rn("path",{d:"M18 8h.01"}),Rn("path",{d:"M6 8h.01"}),Rn("path",{d:"M7 16h10"}),Rn("path",{d:"M8 12h.01"}),Rn("rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"})]}),Sd=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:e,style:{transform:"rotate(180deg)"},children:[Rn("circle",{cx:"12",cy:"12",r:"10"}),Rn("path",{d:"m4.9 4.9 14.2 14.2"})]}),Cd=({className:e="",size:t=24})=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:t,height:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:e,children:[Rn("polyline",{points:"22 17 13.5 8.5 8.5 13.5 2 7"}),Rn("polyline",{points:"16 17 22 17 22 11"})]}),Td=({children:e,triggerContent:t,wrapperProps:n})=>{const[r,o]=Ge("closed"),[i,a]=Ge(null),[s,l]=Ge({width:window.innerWidth,height:window.innerHeight}),c=Xe(null),d=Xe(null),u=Qe(zu),p=Xe(!1);Je(()=>{const e=()=>{l({width:window.innerWidth,height:window.innerHeight}),h()};return window.addEventListener("resize",e),()=>window.removeEventListener("resize",e)},[]);const h=()=>{if(c.current&&u){const e=c.current.getBoundingClientRect(),t=u.getBoundingClientRect(),n=e.left+e.width/2,r=e.top,o=new DOMRect(n-t.left,r-t.top,e.width,e.height);a(o)}};Je(()=>{h()},[c.current]),Je(()=>{if("opening"===r){const e=setTimeout(()=>o("open"),120);return()=>clearTimeout(e)}if("closing"===r){const e=setTimeout(()=>o("closed"),120);return()=>clearTimeout(e)}},[r]),Je(()=>{const e=setInterval(()=>{p.current||"closed"===r||o("closing")},1e3);return()=>clearInterval(e)},[r]);const m=(()=>{var e;if(!i||!u)return{top:0,left:0};const t=u.getBoundingClientRect(),n=(null==(e=d.current)?void 0:e.offsetHeight)||40,r=i.x+t.left,o=i.y+t.top;let a=r,l=o-4;return a-87.5<5?a=92.5:a+87.5>s.width-5&&(a=s.width-5-87.5),l-n<5&&(l=o+i.height+4),{top:l-t.top,left:a-t.left}})();return Rn(me,{children:[u&&i&&"closed"!==r&&(f=Rn("div",{ref:d,className:lo(["absolute z-100 bg-white text-black rounded-lg px-3 py-2 shadow-lg","transition-[opacity] duration-120 ease-out",'after:content-[""] after:absolute after:top-[100%]',"after:left-1/2 after:-translate-x-1/2","after:w-[10px] after:h-[6px]","after:border-l-[5px] after:border-l-transparent","after:border-r-[5px] after:border-r-transparent","after:border-t-[6px] after:border-t-white","pointer-events-none","opening"===r||"closing"===r?"opacity-0":"opacity-100"]),style:{top:m.top+"px",left:m.left+"px",transform:`translate(-50%, calc(-100% - 4px)) scale(${"open"===r?1:.97})`,minWidth:"175px",willChange:"opacity, transform"},children:e}),g=u,v=pe(yn,{__v:f,h:g}),v.containerInfo=g,v),Rn("div",{ref:c,onMouseEnter:()=>{p.current=!0,h(),o("opening")},onMouseLeave:()=>{p.current=!1,h(),o("closing")},...n,children:t})]});var f,g,v},Ed=({selectedEvent:e})=>{const{notificationState:t,setNotificationState:n,setRoute:r}=fd();return Rn("div",{className:lo(["flex w-full justify-between items-center px-3 py-2 text-xs"]),children:[Rn("div",{className:lo(["bg-[#18181B] flex items-center gap-x-1 p-1 rounded-sm"]),children:[Rn("button",{onClick:()=>{r({route:"render-visualization",routeMessage:null})},className:lo(["w-1/2 flex items-center justify-center whitespace-nowrap py-[5px] px-1 gap-x-1","render-visualization"===t.route||"render-explanation"===t.route?"text-white bg-[#7521c8] rounded-sm":"text-[#6E6E77] bg-[#18181B] rounded-sm"]),children:"Ranked"}),Rn("button",{onClick:()=>{r({route:"other-visualization",routeMessage:null})},className:lo(["w-1/2 flex items-center justify-center whitespace-nowrap py-[5px] px-1 gap-x-1","other-visualization"===t.route?"text-white bg-[#7521c8] rounded-sm":"text-[#6E6E77] bg-[#18181B] rounded-sm"]),children:"Overview"}),Rn("button",{onClick:()=>{r({route:"optimize",routeMessage:null})},className:lo(["w-1/2 flex items-center justify-center whitespace-nowrap py-[5px] px-1 gap-x-1","optimize"===t.route?"text-white bg-[#7521c8] rounded-sm":"text-[#6E6E77] bg-[#18181B] rounded-sm"]),children:Rn("span",{children:"Prompts"})})]}),Rn(Td,{triggerContent:Rn("button",{onClick:()=>{n(e=>{e.audioNotificationsOptions.enabled&&"closed"!==e.audioNotificationsOptions.audioContext.state&&e.audioNotificationsOptions.audioContext.close();const t=e.audioNotificationsOptions.enabled;localStorage.setItem("react-scan-notifications-audio",String(!t));const n=new AudioContext;return e.audioNotificationsOptions.enabled||nn(n),t&&n.close(),{...e,audioNotificationsOptions:t?{audioContext:null,enabled:!1}:{audioContext:n,enabled:!0}}})},className:"ml-auto",children:Rn("div",{className:lo(["flex gap-x-2 justify-center items-center text-[#6E6E77]"]),children:[Rn("span",{children:"Alerts"}),t.audioNotificationsOptions.enabled?Rn(xd,{size:16,className:"text-[#6E6E77]"}):Rn(yd,{size:16,className:"text-[#6E6E77]"})]})}),children:Rn(me,{children:"Play a chime when a slowdown is recorded"})})]})},zd=e=>{let t="";return e.toSorted((e,t)=>t.totalTime-e.totalTime).slice(0,30).filter(e=>e.totalTime>5).forEach(e=>{let n="";n+="Component Name:",n+=e.name,n+="\n",n+=`Rendered: ${e.count} times\n`,n+=`Sum of self times for ${e.name} is ${e.totalTime.toFixed(0)}ms\n`,e.changes.props.length>0&&(n+=`Changed props for all ${e.name} instances ("name:count" pairs)\n`,e.changes.props.forEach(e=>{n+=`${e.name}:${e.count}x\n`})),e.changes.state.length>0&&(n+=`Changed state for all ${e.name} instances ("hook index:count" pairs)\n`,e.changes.state.forEach(e=>{n+=`${e.index}:${e.count}x\n`})),e.changes.context.length>0&&(n+=`Changed context for all ${e.name} instances ("context display name (if exists):count" pairs)\n`,e.changes.context.forEach(e=>{n+=`${e.name}:${e.count}x\n`})),t+=n,t+="\n"}),t},Ad=(e,t)=>Oc(()=>{switch(e){case"data":switch(t.kind){case"dropped-frames":return(({renderTime:e,otherTime:t,formattedReactData:n})=>`I will provide you with a set of high level, and low level performance data about a large frame drop in a React App:\n### High level\n- react component render time: ${e.toFixed(0)}ms\n- how long it took to run everything else (other JavaScript, hooks like useEffect, style recalculations, layerization, paint & commit and everything else the browser might do to draw a new frame after javascript mutates the DOM): ${t}ms\n\n### Low level\nWe also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered.\n${n}`)({formattedReactData:zd(t.groupedFiberRenders),renderTime:t.groupedFiberRenders.reduce((e,t)=>e+t.totalTime,0),otherTime:t.timing.otherTime});case"interaction":return(({renderTime:e,eHandlerTimeExcludingRenders:t,toRafTime:n,commitTime:r,framePresentTime:o,formattedReactData:i})=>`I will provide you with a set of high level, and low level performance data about an interaction in a React App:\n### High level\n- react component render time: ${e.toFixed(0)}ms\n- how long it took to run javascript event handlers (EXCLUDING REACT RENDERS): ${t.toFixed(0)}ms\n- how long it took from the last event handler time, to the last request animation frame: ${n.toFixed(0)}ms\n\t- things like prepaint, style recalculations, layerization, async web API's like observers may occur during this time\n- how long it took from the last request animation frame to when the dom was committed: ${r.toFixed(0)}ms\n\t- during this period you will see paint, commit, potential style recalcs, and other misc browser activity. Frequently high times here imply css that makes the browser do a lot of work, or mutating expensive dom properties during the event handler stage. This can be many things, but it narrows the problem scope significantly when this is high\n${null===o?"":`- how long it took from dom commit for the frame to be presented: ${o.toFixed(0)}ms. This is when information about how to paint the next frame is sent to the compositor threads, and when the GPU does work. If this is high, look for issues that may be a bottleneck for operations occurring during this time`}\n\n### Low level\nWe also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered.\n${i}`)({commitTime:t.timing.frameConstruction,eHandlerTimeExcludingRenders:t.timing.otherJSTime,formattedReactData:zd(t.groupedFiberRenders),framePresentTime:t.timing.frameDraw,renderTime:t.groupedFiberRenders.reduce((e,t)=>e+t.totalTime,0),toRafTime:t.timing.framePreparation})}case"explanation":switch(t.kind){case"dropped-frames":return(({renderTime:e,otherTime:t,formattedReactData:n})=>`Your goal will be to help me find the source of a performance problem in a React App. I collected a large dataset about this specific performance problem.\n\nWe have the high level time of how much react spent rendering, and what else the browser spent time on during this slowdown\n\n- react component render time: ${e.toFixed(0)}ms\n- other time (other JavaScript, hooks like useEffect, style recalculations, layerization, paint & commit and everything else the browser might do to draw a new frame after javascript mutates the DOM): ${t}ms\n\n\nWe also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered.\n\n${n}\n\nYou may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation\n\nIt's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So a flow we can go through is:\n- find the most expensive components\n- see what's causing them to render\n- determine how you can make those state/props/context not change for a large set of the renders\n- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. \n\n\nAn important thing to note is that if you see a lot of react renders (some components with very high render counts), but other time is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run outside of what we profile (just react render time).\n\nIt's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one, and this can add significant overhead when thousands of effects ran.\n\nIf it's not possible to explain the root problem from this data, please ask me for more data explicitly, and what we would need to know to find the source of the performance problem.\n`)({formattedReactData:zd(t.groupedFiberRenders),renderTime:t.groupedFiberRenders.reduce((e,t)=>e+t.totalTime,0),otherTime:t.timing.otherTime});case"interaction":return(({interactionType:e,name:t,time:n,renderTime:r,eHandlerTimeExcludingRenders:o,toRafTime:i,commitTime:a,framePresentTime:s,formattedReactData:l})=>`Your goal will be to help me find the source of a performance problem. I collected a large dataset about this specific performance problem.\n\nThere was a ${e} on a component named ${t}. This means, roughly, the component that handled the ${e} event was named ${t}.\n\nWe have a set of high level, and low level data about the performance issue.\n\nThe click took ${n.toFixed(0)}ms from interaction start, to when a new frame was presented to a user.\n\nWe also provide you with a breakdown of what the browser spent time on during the period of interaction start to frame presentation.\n\n- react component render time: ${r.toFixed(0)}ms\n- how long it took to run javascript event handlers (EXCLUDING REACT RENDERS): ${o.toFixed(0)}ms\n- how long it took from the last event handler time, to the last request animation frame: ${i.toFixed(0)}ms\n\t- things like prepaint, style recalculations, layerization, async web API's like observers may occur during this time\n- how long it took from the last request animation frame to when the dom was committed: ${a.toFixed(0)}ms\n\t- during this period you will see paint, commit, potential style recalcs, and other misc browser activity. Frequently high times here imply css that makes the browser do a lot of work, or mutating expensive dom properties during the event handler stage. This can be many things, but it narrows the problem scope significantly when this is high\n${null===s?"":`- how long it took from dom commit for the frame to be presented: ${s.toFixed(0)}ms. This is when information about how to paint the next frame is sent to the compositor threads, and when the GPU does work. If this is high, look for issues that may be a bottleneck for operations occurring during this time`}\n\nWe also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered.\n\n${l}\n\n\nYou may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation\n\nIt's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So a flow we can go through is:\n- find the most expensive components\n- see what's causing them to render\n- determine how you can make those state/props/context not change for a large set of the renders\n- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. \n\n\nAn important thing to note is that if you see a lot of react renders (some components with very high render counts), but javascript excluding renders is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run during the JS event handler period.\n\nIt's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one. And it may also be the case the comparison of the hooks dependency can be expensive, and that would not be tracked in render time.\n\nIf it's not possible to explain the root problem from this data, please ask me for more data explicitly, and what we would need to know to find the source of the performance problem.\n`)({commitTime:t.timing.frameConstruction,eHandlerTimeExcludingRenders:t.timing.otherJSTime,formattedReactData:zd(t.groupedFiberRenders),framePresentTime:t.timing.frameDraw,interactionType:t.type,name:pd(t.componentPath),renderTime:t.groupedFiberRenders.reduce((e,t)=>e+t.totalTime,0),time:hd(t.timing),toRafTime:t.timing.framePreparation})}case"fix":switch(t.kind){case"dropped-frames":return(({renderTime:e,otherTime:t,formattedReactData:n})=>`You will attempt to implement a performance improvement to a large slowdown in a react app\n\nYour should split your goals into 2 parts:\n- identifying the problem\n- fixing the problem\n\t- it is okay to implement a fix even if you aren't 100% sure the fix solves the performance problem. When you aren't sure, you should tell the user to try repeating the interaction, and feeding the "Formatted Data" in the React Scan notifications optimize tab. This allows you to start a debugging flow with the user, where you attempt a fix, and observe the result. The user may make a mistake when they pass you the formatted data, so must make sure, given the data passed to you, that the associated data ties to the same interaction you were trying to debug.\n\nMake sure to check if the user has the react compiler enabled (project dependent, configured through build tool), so you don't unnecessarily memoize components. If it is, you do not need to worry about memoizing user components\n\nOne challenge you may face is the performance problem lies in a node_module, not in user code. If you are confident the problem originates because of a node_module, there are multiple strategies, which are context dependent:\n- you can try to work around the problem, knowing which module is slow\n- you can determine if its possible to resolve the problem in the node_module by modifying non node_module code\n- you can monkey patch the node_module to experiment and see if it's really the problem (you can modify a functions properties to hijack the call for example)\n- you can determine if it's feasible to replace whatever node_module is causing the problem with a performant option (this is an extreme)\n\n\nWe have the high level time of how much react spent rendering, and what else the browser spent time on during this slowdown\n\n- react component render time: ${e.toFixed(0)}ms\n- other time: ${t}ms\n\n\nWe also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered.\n\n${n}\n\nYou may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation\n\nIt's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So the flow should be:\n- find the most expensive components\n- see what's causing them to render\n- determine how you can make those state/props/context not change for a large set of the renders\n- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. \n\nAn important thing to note is that if you see a lot of react renders (some components with very high render counts), but other time is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run outside of what we profile (just react render time).\n\nIt's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one. And it may also be the case the comparison of the hooks dependency can be expensive, and that would not be tracked in render time.\n\nIf a node_module is the component with high renders, you can experiment to see if that component is the root issue (because of hooks). You should use the same instructions for node_module debugging mentioned previously.\n\nIf renders don't seem to be the problem, see if there are any expensive CSS properties being added/mutated, or any expensive DOM Element mutations/new elements being created that could cause this slowdown. \n`)({formattedReactData:zd(t.groupedFiberRenders),renderTime:t.groupedFiberRenders.reduce((e,t)=>e+t.totalTime,0),otherTime:t.timing.otherTime});case"interaction":return(({interactionType:e,name:t,componentPath:n,time:r,renderTime:o,eHandlerTimeExcludingRenders:i,toRafTime:a,commitTime:s,framePresentTime:l,formattedReactData:c})=>`You will attempt to implement a performance improvement to a user interaction in a React app. You will be provided with data about the interaction, and the slow down.\n\nYour should split your goals into 2 parts:\n- identifying the problem\n- fixing the problem\n\t- it is okay to implement a fix even if you aren't 100% sure the fix solves the performance problem. When you aren't sure, you should tell the user to try repeating the interaction, and feeding the "Formatted Data" in the React Scan notifications optimize tab. This allows you to start a debugging flow with the user, where you attempt a fix, and observe the result. The user may make a mistake when they pass you the formatted data, so must make sure, given the data passed to you, that the associated data ties to the same interaction you were trying to debug.\n\n\nMake sure to check if the user has the react compiler enabled (project dependent, configured through build tool), so you don't unnecessarily memoize components. If it is, you do not need to worry about memoizing user components\n\nOne challenge you may face is the performance problem lies in a node_module, not in user code. If you are confident the problem originates because of a node_module, there are multiple strategies, which are context dependent:\n- you can try to work around the problem, knowing which module is slow\n- you can determine if its possible to resolve the problem in the node_module by modifying non node_module code\n- you can monkey patch the node_module to experiment and see if it's really the problem (you can modify a functions properties to hijack the call for example)\n- you can determine if it's feasible to replace whatever node_module is causing the problem with a performant option (this is an extreme)\n\nThe interaction was a ${e} on the component named ${t}. This component has the following ancestors ${n}. This is the path from the component, to the root. This should be enough information to figure out where this component is in the user's code base\n\nThis path is the component that was clicked, so it should tell you roughly where component had an event handler that triggered a state change.\n\nPlease note that the leaf node of this path might not be user code (if they use a UI library), and they may contain many wrapper components that just pass through children that aren't relevant to the actual click. So make you sure analyze the path and understand what the user code is doing\n\nWe have a set of high level, and low level data about the performance issue.\n\nThe click took ${r.toFixed(0)}ms from interaction start, to when a new frame was presented to a user.\n\nWe also provide you with a breakdown of what the browser spent time on during the period of interaction start to frame presentation.\n\n- react component render time: ${o.toFixed(0)}ms\n- how long it took to run javascript event handlers (EXCLUDING REACT RENDERS): ${i.toFixed(0)}ms\n- how long it took from the last event handler time, to the last request animation frame: ${a.toFixed(0)}ms\n\t- things like prepaint, style recalculations, layerization, async web API's like observers may occur during this time\n- how long it took from the last request animation frame to when the dom was committed: ${s.toFixed(0)}ms\n\t- during this period you will see paint, commit, potential style recalcs, and other misc browser activity. Frequently high times here imply css that makes the browser do a lot of work, or mutating expensive dom properties during the event handler stage. This can be many things, but it narrows the problem scope significantly when this is high\n${null===l?"":`- how long it took from dom commit for the frame to be presented: ${l.toFixed(0)}ms. This is when information about how to paint the next frame is sent to the compositor threads, and when the GPU does work. If this is high, look for issues that may be a bottleneck for operations occurring during this time`}\n\n\nWe also have lower level information about react components, such as their render time, and which props/state/context changed when they re-rendered.\n\n${c}\n\nYou may notice components have many renders, but much fewer props/state/context changes. This normally implies most of the components could have been memoized to avoid computation\n\nIt's also important to remember if a component had no props/state/context change, and it was memoized, it would not render. So the flow should be:\n- find the most expensive components\n- see what's causing them to render\n- determine how you can make those state/props/context not change for a large set of the renders\n- once there are no more changes left, you can memoize the component so it no longer unnecessarily re-renders. \n\nAn important thing to note is that if you see a lot of react renders (some components with very high render counts), but javascript excluding renders is much higher than render time, it is possible that the components with lots of renders run hooks like useEffect/useLayoutEffect, which run during the JS event handler period.\n\nIt's also good to note that react profiles hook times in development, and if many hooks are called (lets say 5,000 components all called a useEffect), it will have to profile every single one. And it may also be the case the comparison of the hooks dependency can be expensive, and that would not be tracked in render time.\n\nIf a node_module is the component with high renders, you can experiment to see if that component is the root issue (because of hooks). You should use the same instructions for node_module debugging mentioned previously.\n\n`)({commitTime:t.timing.frameConstruction,componentPath:t.componentPath.join(">"),eHandlerTimeExcludingRenders:t.timing.otherJSTime,formattedReactData:zd(t.groupedFiberRenders),framePresentTime:t.timing.frameDraw,interactionType:t.type,name:pd(t.componentPath),renderTime:t.groupedFiberRenders.reduce((e,t)=>e+t.totalTime,0),time:hd(t.timing),toRafTime:t.timing.framePreparation})}}}),Md=({selectedEvent:e})=>{const[t,n]=Ge("fix"),[r,o]=Ge(!1);return Rn("div",{className:lo(["w-full h-full"]),children:[Rn("div",{className:lo(["border border-[#27272A] rounded-sm h-4/5 text-xs overflow-hidden"]),children:[Rn("div",{className:lo(["bg-[#18181B] p-1 rounded-t-sm"]),children:Rn("div",{className:lo(["flex items-center gap-x-1"]),children:[Rn("button",{onClick:()=>n("fix"),className:lo(["flex items-center justify-center whitespace-nowrap py-1.5 px-3 rounded-sm","fix"===t?"text-white bg-[#7521c8]":"text-[#6E6E77] hover:text-white"]),children:"Fix"}),Rn("button",{onClick:()=>n("explanation"),className:lo(["flex items-center justify-center whitespace-nowrap py-1.5 px-3 rounded-sm","explanation"===t?"text-white bg-[#7521c8]":"text-[#6E6E77] hover:text-white"]),children:"Explanation"}),Rn("button",{onClick:()=>n("data"),className:lo(["flex items-center justify-center whitespace-nowrap py-1.5 px-3 rounded-sm","data"===t?"text-white bg-[#7521c8]":"text-[#6E6E77] hover:text-white"]),children:"Data"})]})}),Rn("div",{className:lo(["overflow-y-auto h-full"]),children:Rn("pre",{className:lo(["p-2 h-full","whitespace-pre-wrap break-words","text-gray-300 font-mono "]),children:Ad(t,e)})})]}),Rn("button",{onClick:async()=>{const n=Ad(t,e);await navigator.clipboard.writeText(n),o(!0),setTimeout(()=>o(!1),1e3)},className:lo(["mt-4 px-4 py-2 bg-[#18181B] text-[#6E6E77] rounded-sm","hover:text-white transition-colors duration-200","flex items-center justify-center gap-x-2 text-xs"]),children:[Rn("span",{children:r?"Copied!":"Copy Prompt"}),Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:lo(["transition-transform duration-200",r&&"scale-110"]),children:r?Rn("path",{d:"M20 6L9 17l-5-5"}):Rn(me,{children:[Rn("rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}),Rn("path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"})]})})]})]})},$d=({selectedEvent:e})=>{var t,n;const[r]=Ge(null!=(t=Vu())&&t),{notificationState:o}=fd(),[i,a]=Ge((null==(n=o.routeMessage)?void 0:n.name)?[o.routeMessage.name]:[]),s=((e,t)=>{switch(e.kind){case"dropped-frames":return[...t?[{name:"Total Processing Time",time:hd(e.timing),color:"bg-red-500",kind:"total-processing-time"}]:[{name:"Renders",time:e.timing.renderTime,color:"bg-purple-500",kind:"render"},{name:"JavaScript, DOM updates, Draw Frame",time:e.timing.otherTime,color:"bg-[#4b4b4b]",kind:"other-frame-drop"}]];case"interaction":return[...t?[]:[{name:"Renders",time:e.timing.renderTime,color:"bg-purple-500",kind:"render"}],{name:t?"React Renders, Hooks, Other JavaScript":"JavaScript/React Hooks ",time:e.timing.otherJSTime,color:"bg-[#EFD81A]",kind:"other-javascript"},{name:"Update DOM and Draw New Frame",time:hd(e.timing)-e.timing.renderTime-e.timing.otherJSTime,color:"bg-[#1D3A66]",kind:"other-not-javascript"}]}})(e,r),l=Qe(zu);Je(()=>{var e;if(null==(e=o.routeMessage)?void 0:e.name){const e=null==l?void 0:l.querySelector("#overview-scroll-container"),t=null==l?void 0:l.querySelector(`#react-scan-overview-bar-${o.routeMessage.name}`);if(e&&t){const n=t.getBoundingClientRect().top-e.getBoundingClientRect().top;e.scrollTop=e.scrollTop+n}}},[o.route]),Je(()=>{"other-visualization"===o.route&&a(e=>{var t;return(null==(t=o.routeMessage)?void 0:t.name)?[o.routeMessage.name]:e})},[o.route]);const c=s.reduce((e,t)=>e+t.time,0);return Rn("div",{className:"rounded-sm border border-zinc-800 text-xs",children:[Rn("div",{className:"p-2 border-b border-zinc-800 bg-zinc-900/50",children:Rn("div",{className:"flex items-center justify-between",children:[Rn("h3",{className:"text-xs font-medium",children:"What was time spent on?"}),Rn("span",{className:"text-xs text-zinc-400",children:["Total: ",c.toFixed(0),"ms"]})]})}),Rn("div",{className:"divide-y divide-zinc-800",children:s.map(t=>{const n=i.includes(t.kind);return Rn("div",{id:`react-scan-overview-bar-${t.kind}`,children:[Rn("button",{onClick:()=>a(e=>e.includes(t.kind)?e.filter(e=>e!==t.kind):[...e,t.kind]),className:"w-full px-3 py-2 flex items-center gap-4 hover:bg-zinc-800/50 transition-colors",children:Rn("div",{className:"flex-1",children:[Rn("div",{className:"flex items-center justify-between mb-2",children:[Rn("div",{className:"flex items-center gap-0.5",children:[Rn("svg",{className:"h-4 w-4 text-zinc-400 transition-transform "+(n?"rotate-90":""),fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:Rn("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 5l7 7-7 7"})}),Rn("span",{className:"font-medium flex items-center text-left",children:t.name})]}),Rn("span",{className:" text-zinc-400",children:[t.time.toFixed(0),"ms"]})]}),Rn("div",{className:"h-1 bg-zinc-800 rounded-full overflow-hidden",children:Rn("div",{className:`h-full ${t.color} transition-all`,style:{width:t.time/c*100+"%"}})})]})}),n&&Rn("div",{className:"bg-zinc-900/30 border-t border-zinc-800 px-2.5 py-3",children:Rn("p",{className:" text-zinc-400 mb-4 text-xs",children:Oc(()=>{switch(e.kind){case"interaction":switch(t.kind){case"render":return Rn(jd,{input:Od(e)});case"other-javascript":return Rn(jd,{input:Dd(e)});case"other-not-javascript":return Rn(jd,{input:Fd(e)})}case"dropped-frames":switch(t.kind){case"total-processing-time":return Rn(jd,{input:{kind:"total-processing",data:{time:hd(e.timing)}}});case"render":return Rn(me,{children:Rn(jd,{input:{kind:"render",data:{topByTime:e.groupedFiberRenders.toSorted((e,t)=>t.totalTime-e.totalTime).slice(0,3).map(t=>({name:t.name,percentage:t.totalTime/hd(e.timing)}))}}})});case"other-frame-drop":return Rn(jd,{input:{kind:"other"}})}}})})})]},t.kind)})})]})},Fd=e=>{const t=e.groupedFiberRenders.reduce((e,t)=>e+t.count,0),n=e.timing.renderTime,r=hd(e.timing);return t>100?{kind:"high-render-count-update-dom-draw-frame",data:{count:t,percentageOfTotal:n/r*100,copyButton:Rn(Rd,{})}}:{kind:"update-dom-draw-frame",data:{copyButton:Rn(Rd,{})}}},Rd=()=>{const[e,t]=Ge(!1),{notificationState:n}=fd();return Rn("button",{onClick:async()=>{n.selectedEvent&&(await navigator.clipboard.writeText(Ad("explanation",n.selectedEvent)),t(!0),setTimeout(()=>t(!1),1e3))},className:"bg-zinc-800 flex hover:bg-zinc-700 text-zinc-200 px-2 py-1 rounded gap-x-3",children:[Rn("span",{children:e?"Copied!":"Copy Prompt"}),Rn("svg",{xmlns:"http://www.w3.org/2000/svg",width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:lo(["transition-transform duration-200",e&&"scale-110"]),children:e?Rn("path",{d:"M20 6L9 17l-5-5"}):Rn(me,{children:[Rn("rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}),Rn("path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"})]})})]})},Od=e=>e.timing.renderTime/hd(e.timing)>.3?{kind:"render",data:{topByTime:e.groupedFiberRenders.toSorted((e,t)=>t.totalTime-e.totalTime).slice(0,3).map(t=>({percentage:t.totalTime/hd(e.timing),name:t.name}))}}:{kind:"other"},Dd=e=>{const t=e.groupedFiberRenders.reduce((e,t)=>e+t.count,0);return e.timing.otherJSTime/hd(e.timing)<.2?{kind:"js-explanation-base"}:e.groupedFiberRenders.find(e=>e.count>200)||e.groupedFiberRenders.reduce((e,t)=>e+t.count,0)>500?{kind:"high-render-count-high-js",data:{renderCount:t,topByCount:e.groupedFiberRenders.filter(e=>e.count>100).toSorted((e,t)=>t.count-e.count).slice(0,3)}}:e.timing.otherJSTime/hd(e.timing)>.3?e.timing.renderTime>.2?{kind:"js-explanation-base"}:{kind:"low-render-count-high-js",data:{renderCount:t}}:{kind:"js-explanation-base"}},jd=({input:e})=>{switch(e.kind){case"total-processing":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:["This is the time it took to draw the entire frame that was presented to the user. To be at 60FPS, this number needs to be ","<=16ms"]}),Rn("p",{children:'To debug the issue, check the "Ranked" tab to see if there are significant component renders'}),Rn("p",{children:"On a production React build, React Scan can't access the time it took for component to render. To get that information, run React Scan on a development build"}),Rn("p",{children:["To understand precisely what caused the slowdown while in production, use the"," ",Rn("strong",{children:"Chrome profiler"})," and analyze the function call times."]}),Rn("p",{})]});case"render":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:"This is the time it took React to run components, and internal logic to handle the output of your component."}),Rn("div",{className:lo(["flex flex-col"]),children:[Rn("p",{children:"The slowest components for this time period were:"}),e.data.topByTime.map(e=>Rn("div",{children:[Rn("strong",{children:e.name}),": ",(100*e.percentage).toFixed(0),"% of total"]},e.name))]}),Rn("p",{children:'To view the render times of all your components, and what caused them to render, go to the "Ranked" tab'}),Rn("p",{children:'The "Ranked" tab shows the render times of every component.'}),Rn("p",{children:"The render times of the same components are grouped together into one bar."}),Rn("p",{children:"Clicking the component will show you what props, state, or context caused the component to re-render."})]});case"js-explanation-base":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:"This is the period when JavaScript hooks and other JavaScript outside of React Renders run."}),Rn("p",{children:["The most common culprit for high JS time is expensive hooks, like expensive callbacks inside of ",Rn("code",{children:"useEffect"}),"'s or a large number of useEffect's called, but this can also be JavaScript event handlers (",Rn("code",{children:"'onclick'"}),", ",Rn("code",{children:"'onchange'"}),") that performed expensive computation."]}),Rn("p",{children:"If you have lots of components rendering that call hooks, like useEffect, it can add significant overhead even if the callbacks are not expensive. If this is the case, you can try optimizing the renders of those components to avoid the hook from having to run."}),Rn("p",{children:["You should profile your app using the ",Rn("strong",{children:"Chrome DevTools profiler"})," to learn exactly which functions took the longest to execute."]})]});case"high-render-count-high-js":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:"This is the period when JavaScript hooks and other JavaScript outside of React Renders run."}),0===e.data.renderCount?Rn(me,{children:[Rn("p",{children:"There were no renders, which means nothing related to React caused this slowdown. The most likely cause of the slowdown is a slow JavaScript event handler, or code related to a Web API"}),Rn("p",{children:["You should try to reproduce the slowdown while profiling your website with the",Rn("strong",{children:"Chrome DevTools profiler"})," to see exactly what functions took the longest to execute."]})]}):Rn(me,{children:[" ",Rn("p",{children:["There were ",Rn("strong",{children:e.data.renderCount})," renders, which could have contributed to the high JavaScript/Hook time if they ran lots of hooks, like"," ",Rn("code",{children:"useEffects"}),"."]}),Rn("div",{className:lo(["flex flex-col"]),children:[Rn("p",{children:"You should try optimizing the renders of:"}),e.data.topByCount.map(e=>Rn("div",{children:["- ",Rn("strong",{children:e.name})," (rendered ",e.count,"x)"]},e.name))]}),"and then checking if the problem still exists.",Rn("p",{children:["You can also try profiling your app using the"," ",Rn("strong",{children:"Chrome DevTools profiler"})," to see exactly what functions took the longest to execute."]})]})]});case"low-render-count-high-js":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:"This is the period when JavaScript hooks and other JavaScript outside of React Renders run."}),Rn("p",{children:["There were only ",Rn("strong",{children:e.data.renderCount})," renders detected, which means either you had very expensive hooks like ",Rn("code",{children:"useEffect"}),"/",Rn("code",{children:"useLayoutEffect"}),", or there is other JavaScript running during this interaction that took up the majority of the time."]}),Rn("p",{children:["To understand precisely what caused the slowdown, use the"," ",Rn("strong",{children:"Chrome profiler"})," and analyze the function call times."]})]});case"high-render-count-update-dom-draw-frame":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:"These are the calculations the browser is forced to do in response to the JavaScript that ran during the interaction."}),Rn("p",{children:"This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations."}),Rn("p",{children:["During this interaction, there were ",Rn("strong",{children:e.data.count})," renders, which was ",Rn("strong",{children:[e.data.percentageOfTotal.toFixed(0),"%"]})," of the time spent processing"]}),Rn("p",{children:"The work performed as a result of the renders may have forced the browser to spend a lot of time to draw the next frame."}),Rn("p",{children:'You can try optimizing the renders to see if the performance problem still exists using the "Ranked" tab.'}),Rn("p",{children:"If you use an AI-based code editor, you can export the performance data collected as a prompt."}),Rn("p",{children:e.data.copyButton}),Rn("p",{children:"Provide this formatted data to the model and ask it to find, or fix, what could be causing this performance problem."}),Rn("p",{children:'For a larger selection of prompts, try the "Prompts" tab'})]});case"update-dom-draw-frame":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:"These are the calculations the browser is forced to do in response to the JavaScript that ran during the interaction."}),Rn("p",{children:"This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations."}),Rn("p",{children:"If you use an AI-based code editor, you can export the performance data collected as a prompt."}),Rn("p",{children:e.data.copyButton}),Rn("p",{children:"Provide this formatted data to the model and ask it to find, or fix, what could be causing this performance problem."}),Rn("p",{children:'For a larger selection of prompts, try the "Prompts" tab'})]});case"other":return Rn("div",{className:lo(["text-[#E4E4E7] text-[10px] leading-6 flex flex-col gap-y-2"]),children:[Rn("p",{children:["This is the time it took to run everything other than React renders. This can be hooks like ",Rn("code",{children:"useEffect"}),", other JavaScript not part of React, or work the browser has to do to update the DOM and draw the next frame."]}),Rn("p",{children:["To get a better picture of what happened, profile your app using the"," ",Rn("strong",{children:"Chrome profiler"})," when the performance problem arises."]})]})}},Ld=null,Pd=null,Id=_t({kind:"idle",current:null}),Wd=null,Ud=0,Hd=1/60,Bd=()=>{Wd&&cancelAnimationFrame(Wd),Wd=requestAnimationFrame(e=>{if(!Ld||!Pd)return;const t=Ud?Math.min((e-Ud)/1e3,.05):Hd;Ud=e;const n=1.8*t;Pd.clearRect(0,0,Ld.width,Ld.height);const r="hsl(271, 76%, 53%)",o=Id.value,{alpha:i,current:a}=Oc(()=>{var e,t,n;switch(o.kind){case"transition":{const t=(null==(e=o.current)?void 0:e.alpha)&&o.current.alpha>0?o.current:o.transitionTo;return{alpha:t?t.alpha:0,current:t}}case"move-out":return{alpha:null!=(n=null==(t=o.current)?void 0:t.alpha)?n:0,current:o.current};case"idle":return{alpha:1,current:o.current}}});switch(null==a||a.rects.forEach(e=>{Pd&&(Pd.shadowColor=r,Pd.shadowBlur=6,Pd.strokeStyle=r,Pd.lineWidth=2,Pd.globalAlpha=i,Pd.beginPath(),Pd.rect(e.left,e.top,e.width,e.height),Pd.stroke(),Pd.shadowBlur=0,Pd.beginPath(),Pd.rect(e.left,e.top,e.width,e.height),Pd.stroke())}),o.kind){case"move-out":return 0===o.current.alpha?(Id.value={kind:"idle",current:null},void(Ud=0)):(o.current.alpha<=.01&&(o.current.alpha=0),o.current.alpha=Math.max(0,o.current.alpha-n),void Bd());case"transition":if(o.current&&o.current.alpha>0)return o.current.alpha=Math.max(0,o.current.alpha-n),void Bd();if(1===o.transitionTo.alpha)return Id.value={kind:"idle",current:o.transitionTo},void(Ud=0);o.transitionTo.alpha=Math.min(o.transitionTo.alpha+n,1),Bd();case"idle":return void(Ud=0)}})},Vd=null;function qd(){(null==Ld?void 0:Ld.parentNode)&&Ld.parentNode.removeChild(Ld),Ld=null,Pd=null}var Gd,Jd=()=>{var e,t;const n=Id.value.current?Id.value.current:"transition"===Id.value.kind?Id.value.transitionTo:null;n&&("transition"!==Id.value.kind?Id.value={kind:"move-out",current:{alpha:0,...n}}:Id.value={kind:"move-out",current:0===(null==(e=Id.value.current)?void 0:e.alpha)?Id.value.transitionTo:null!=(t=Id.value.current)?t:Id.value.transitionTo})},Yd=({selectedEvent:e})=>{const t=hd(e.timing),n=t-e.timing.renderTime,[r]=Ge(Vu()),o=e.groupedFiberRenders.map(e=>({event:e,kind:"render",totalTime:r?e.count:e.totalTime})),i=Oc(()=>{switch(e.kind){case"dropped-frames":return e.timing.renderTime/t<.1;case"interaction":return(e.timing.otherJSTime+e.timing.renderTime)/t<.2}});"interaction"!==e.kind||r||o.push({kind:"other-javascript",totalTime:e.timing.otherJSTime}),i&&!r&&("interaction"===e.kind?o.push({kind:"other-not-javascript",totalTime:hd(e.timing)-e.timing.renderTime-e.timing.otherJSTime}):o.push({kind:"other-frame-drop",totalTime:n}));const a=Xe({lastCallAt:null,timer:null}),s=o.reduce((e,t)=>e+t.totalTime,0);return Rn("div",{className:lo(["flex flex-col h-full w-full gap-y-1"]),children:[Oc(()=>r&&0===o.length?Rn("div",{className:"flex flex-col items-center justify-center h-full text-zinc-400",children:[Rn("p",{className:"text-sm w-full text-left text-white mb-1.5",children:"No data available"}),Rn("p",{className:"text-x w-full text-lefts",children:"No data was collected during this period"})]}):0===o.length?Rn("div",{className:"flex flex-col items-center justify-center h-full text-zinc-400",children:[Rn("p",{className:"text-sm w-full text-left text-white mb-1.5",children:"No renders collected"}),Rn("p",{className:"text-x w-full text-lefts",children:"There were no renders during this period"})]}):void 0),o.toSorted((e,t)=>t.totalTime-e.totalTime).map(e=>Rn(Xd,{bars:o,bar:e,debouncedMouseEnter:a,totalBarTime:s,isProduction:r},"render"===e.kind?e.event.id:e.kind))]})},Xd=({bar:e,debouncedMouseEnter:t,totalBarTime:n,isProduction:r,bars:o,depth:i=0})=>{const{setNotificationState:a,setRoute:s}=fd(),[l,c]=Ge(!1),d="render"!==e.kind||0===e.event.parents.size,u=o.filter(t=>"render"===t.kind&&"render"===e.kind&&(e.event.parents.has(t.event.name)&&t.event.name!==e.event.name)),p="render"===e.kind?Array.from(e.event.parents).filter(e=>!o.some(t=>"render"===t.kind&&t.event.name===e)):[];return Rn("div",{className:"w-full",children:[Rn("div",{className:lo(["w-full flex items-center relative text-xs min-w-0"]),children:[Rn("button",{onMouseLeave:()=>{t.current.timer&&clearTimeout(t.current.timer),Jd()},onMouseEnter:async()=>{const n=async()=>{if(t.current.lastCallAt=Date.now(),"render"!==e.kind){const e=Id.value.current?Id.value.current:"transition"===Id.value.kind?Id.value.transitionTo:null;return e?void(Id.value={kind:"move-out",current:{alpha:0,...e}}):void(Id.value={kind:"idle",current:null})}const n=Id.value,r=Oc(()=>{switch(n.kind){case"transition":return n.transitionTo;case"idle":case"move-out":return n.current}}),o=[];if("transition"===n.kind){const t=(e=>e.current&&e.current.alpha>0?"fading-out":"fading-in")(n);Oc(()=>{switch(t){case"fading-in":return void(Id.value={kind:"transition",current:n.transitionTo,transitionTo:{rects:o,alpha:0,name:e.event.name}});case"fading-out":return void(Id.value={kind:"transition",current:Id.value.current?{alpha:0,...Id.value.current}:null,transitionTo:{rects:o,alpha:0,name:e.event.name}})}})}else Id.value={kind:"transition",transitionTo:{rects:o,alpha:0,name:e.event.name},current:r?{alpha:0,...r}:null};const i=e.event.elements.filter(e=>e instanceof Element);for await(const e of ja(i))e.forEach(({boundingClientRect:e})=>{o.push(e)}),Bd()};if(t.current.lastCallAt&&Date.now()-t.current.lastCallAt<200)return t.current.timer&&clearTimeout(t.current.timer),void(t.current.timer=setTimeout(()=>{n()},200));n()},onClick:()=>{"render"===e.kind?(a(t=>({...t,selectedFiber:e.event})),s({route:"render-explanation",routeMessage:null})):s({route:"other-visualization",routeMessage:{kind:"auto-open-overview-accordion",name:e.kind}})},className:lo(["h-full w-[90%] flex items-center hover:bg-[#0f0f0f] rounded-l-md min-w-0 relative"]),children:[Rn("div",{style:{minWidth:"fit-content",width:e.totalTime/n*100+"%"},className:lo(["flex items-center rounded-sm text-white text-xs h-[28px] shrink-0","render"===e.kind&&"bg-[#412162] group-hover:bg-[#5b2d89]","other-frame-drop"===e.kind&&"bg-[#44444a] group-hover:bg-[#6a6a6a]","other-javascript"===e.kind&&"bg-[#efd81a6b] group-hover:bg-[#efda1a2f]","other-not-javascript"===e.kind&&"bg-[#214379d4] group-hover:bg-[#21437982]"])}),Rn("div",{className:lo(["absolute inset-0 flex items-center px-2","min-w-0"]),children:Rn("div",{className:"flex items-center gap-x-2 min-w-0 w-full",children:[Rn("span",{className:lo(["truncate"]),children:Oc(()=>{switch(e.kind){case"other-frame-drop":return"JavaScript, DOM updates, Draw Frame";case"other-javascript":return"JavaScript/React Hooks";case"other-not-javascript":return"Update DOM and Draw New Frame";case"render":return e.event.name}})}),"render"===e.kind&&(h=e.event,!h.wasFiberRenderMount&&!h.hasMemoCache&&0===h.changes.context.length&&0===h.changes.props.length&&0===h.changes.state.length)&&Rn("div",{style:{lineHeight:"10px"},className:lo(["px-1 py-0.5 bg-[#6a369e] flex items-center rounded-sm font-semibold text-[8px] shrink-0"]),children:"Memoizable"})]})})]}),Rn("button",{onClick:()=>"render"===e.kind&&!d&&c(!l),className:lo(["flex items-center min-w-fit shrink-0 rounded-r-md h-[28px]",!d&&"hover:bg-[#0f0f0f]","render"!==e.kind||d?"cursor-default":"cursor-pointer"]),children:[Rn("div",{className:"w-[20px] flex items-center justify-center",children:"render"===e.kind&&!d&&Rn(vd,{className:lo("transition-transform",l&&"rotate-90"),size:16})}),Rn("div",{style:{minWidth:d?"fit-content":r?"30px":"60px"},className:"flex items-center justify-end gap-x-1",children:["render"===e.kind&&Rn("span",{className:lo(["text-[10px]"]),children:["x",e.event.count]}),("render"!==e.kind||!r)&&Rn("span",{className:"text-[10px] text-[#7346a0] pr-1",children:[e.totalTime<1?"<1":e.totalTime.toFixed(0),"ms"]})]})]}),0===i&&Rn("div",{className:lo(["absolute right-0 top-1/2 transition-none -translate-y-1/2 bg-white text-black px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity mr-16","pointer-events-none"]),children:"Click to learn more"})]}),l&&(u.length>0||p.length>0)&&Rn("div",{className:"pl-3 flex flex-col gap-y-1 mt-1",children:[u.toSorted((e,t)=>t.totalTime-e.totalTime).map((e,a)=>Rn(Xd,{depth:i+1,bar:e,debouncedMouseEnter:t,totalBarTime:n,isProduction:r,bars:o},a)),p.map(e=>Rn("div",{className:"w-full",children:Rn("div",{className:"w-full flex items-center relative text-xs",children:Rn("div",{className:"h-full w-full flex items-center relative",children:[Rn("div",{className:"flex items-center rounded-sm text-white text-xs h-[28px] w-full"}),Rn("div",{className:"absolute inset-0 flex items-center px-2",children:Rn("span",{className:"truncate whitespace-nowrap text-white/70 w-full",children:e})})]})})},e))]})]});var h},Kd=({selectedEvent:e,selectedFiber:t})=>{const{setRoute:n}=fd(),[r,o]=Ge(!0),[i]=Ge(Vu());Ye(()=>{const e=localStorage.getItem("react-scan-tip-shown"),t="true"===e||"false"!==e&&null;if(null===t)return o(!0),void localStorage.setItem("react-scan-tip-is-shown","true");t||o(!1)},[]);const a=0===t.changes.context.length&&0===t.changes.props.length&&0===t.changes.state.length;return Rn("div",{className:lo(["w-full min-h-fit h-full flex flex-col py-4 pt-0 rounded-sm"]),children:[Rn("div",{className:lo(["flex items-start gap-x-4 "]),children:[Rn("button",{onClick:()=>{n({route:"render-visualization",routeMessage:null})},className:lo(["text-white hover:bg-[#34343b] flex gap-x-1 justify-center items-center mb-4 w-fit px-2.5 py-1.5 text-xs rounded-sm bg-[#18181B]"]),children:[Rn(kd,{size:14})," ",Rn("span",{children:"Overview"})]}),Rn("div",{className:lo(["flex flex-col gap-y-1"]),children:[Rn("div",{className:lo(["text-sm font-bold text-white overflow-x-hidden"]),children:Rn("div",{className:"flex items-center gap-x-2 truncate",children:t.name})}),Rn("div",{className:lo(["flex gap-x-2"]),children:[!i&&Rn(me,{children:Rn("div",{className:lo(["text-xs text-gray-400"]),children:["• Render time: ",t.totalTime.toFixed(0),"ms"]})}),Rn("div",{className:lo(["text-xs text-gray-400 mb-4"]),children:["• Renders: ",t.count,"x"]})]})]})]}),r&&!a&&Rn("div",{className:lo(["w-full mb-4 bg-[#0A0A0A] border border-[#27272A] rounded-sm overflow-hidden flex relative"]),children:[Rn("button",{onClick:()=>{o(!1),localStorage.setItem("react-scan-tip-shown","false")},className:lo(["absolute right-2 top-2 rounded-sm p-1 hover:bg-[#18181B]"]),children:Rn(bd,{size:12})}),Rn("div",{className:lo(["w-1 bg-[#d36cff]"])}),Rn("div",{className:lo(["flex-1"]),children:[Rn("div",{className:lo(["px-3 py-2 text-gray-100 text-xs font-semibold"]),children:"How to stop renders"}),Rn("div",{className:lo(["px-3 pb-2 text-gray-400 text-[10px]"]),children:"Stop the following props, state and context from changing between renders, and wrap the component in React.memo if not already"})]})]}),a&&Rn("div",{className:lo(["w-full mb-4 bg-[#0A0A0A] border border-[#27272A] rounded-sm overflow-hidden flex"]),children:[Rn("div",{className:lo(["w-1 bg-[#d36cff]"])}),Rn("div",{className:lo(["flex-1"]),children:[Rn("div",{className:lo(["px-3 py-2 text-gray-100 text-sm font-semibold"]),children:"No changes detected"}),Rn("div",{className:lo(["px-3 pb-2 text-gray-400 text-xs"]),children:"This component would not have rendered if it was memoized"})]})]}),Rn("div",{className:lo(["flex w-full"]),children:[Rn("div",{className:lo(["flex flex-col border border-[#27272A] rounded-l-sm overflow-hidden w-1/3"]),children:[Rn("div",{className:lo(["text-[14px] font-semibold px-2 py-2 bg-[#18181B] text-white flex justify-center"]),children:"Changed Props"}),t.changes.props.length>0?t.changes.props.toSorted((e,t)=>t.count-e.count).map(e=>Rn("div",{className:lo(["flex flex-col justify-between items-center border-t overflow-x-auto border-[#27272A] px-1 py-1 text-wrap bg-[#0A0A0A] text-[10px]"]),children:[Rn("span",{className:lo(["text-white "]),children:e.name}),Rn("div",{className:lo([" text-[8px] text-[#d36cff] pl-1 py-1 "]),children:[e.count,"/",t.count,"x"]})]},e.name)):Rn("div",{className:lo(["flex items-center justify-center h-full bg-[#0A0A0A] text-[#A1A1AA] border-t border-[#27272A]"]),children:"No changes"})]}),Rn("div",{className:lo(["flex flex-col border border-[#27272A] border-l-0 overflow-hidden w-1/3"]),children:[Rn("div",{className:lo([" text-[14px] font-semibold px-2 py-2 bg-[#18181B] text-white flex justify-center"]),children:"Changed State"}),t.changes.state.length>0?t.changes.state.toSorted((e,t)=>t.count-e.count).map(e=>Rn("div",{className:lo(["flex flex-col justify-between items-center border-t overflow-x-auto border-[#27272A] px-1 py-1 text-wrap bg-[#0A0A0A] text-[10px]"]),children:[Rn("span",{className:lo(["text-white "]),children:["index ",e.index]}),Rn("div",{className:lo(["rounded-full text-[#d36cff] pl-1 py-1 text-[8px]"]),children:[e.count,"/",t.count,"x"]})]},e.index)):Rn("div",{className:lo(["flex items-center justify-center h-full bg-[#0A0A0A] text-[#A1A1AA] border-t border-[#27272A]"]),children:"No changes"})]}),Rn("div",{className:lo(["flex flex-col border border-[#27272A] border-l-0 rounded-r-sm overflow-hidden w-1/3"]),children:[Rn("div",{className:lo([" text-[14px] font-semibold px-2 py-2 bg-[#18181B] text-white flex justify-center"]),children:"Changed Context"}),t.changes.context.length>0?t.changes.context.toSorted((e,t)=>t.count-e.count).map(e=>Rn("div",{className:lo(["flex flex-col justify-between items-center border-t border-[#27272A] px-1 py-1 bg-[#0A0A0A] text-[10px] overflow-x-auto"]),children:[Rn("span",{className:lo(["text-white "]),children:e.name}),Rn("div",{className:lo(["rounded-full text-[#d36cff] pl-1 py-1 text-[8px] text-wrap"]),children:[e.count,"/",t.count,"x"]})]},e.name)):Rn("div",{className:lo(["flex items-center justify-center h-full bg-[#0A0A0A] text-[#A1A1AA] border-t border-[#27272A] py-2"]),children:"No changes"})]})]})]})},Zd=()=>{const{notificationState:e,setNotificationState:t}=fd(),[n,r]=Ge("..."),o=Xe(null);if(Je(()=>{const e=setInterval(()=>{r(e=>"..."===e?"":e+".")},500);return()=>clearInterval(e)},[]),!e.selectedEvent)return Rn("div",{ref:o,className:lo(["h-full w-full flex flex-col items-center justify-center relative py-2 px-4"]),children:[Rn("div",{className:lo(["p-2 flex justify-center items-center border-[#27272A] absolute top-0 right-0"]),children:Rn("button",{onClick:()=>{_o.value={view:"none"}},children:Rn(bd,{size:18,className:"text-[#6F6F78]"})})}),Rn("div",{className:lo(["flex flex-col items-start pt-5 bg-[#0A0A0A] p-5 rounded-sm max-w-md"," shadow-lg"]),children:Rn("div",{className:lo(["flex flex-col items-start gap-y-4"]),children:[Rn("div",{className:lo(["flex items-center"]),children:Rn("span",{className:lo(["text-zinc-400 font-medium text-[17px]"]),children:["Scanning for slowdowns",n]})}),0!==e.events.length&&Rn("p",{className:lo(["text-xs"]),children:["Click on an item in the"," ",Rn("span",{className:lo(["text-purple-400"]),children:"History"})," list to get started"]}),Rn("p",{className:lo(["text-zinc-600 text-xs"]),children:"You don't need to keep this panel open for React Scan to record slowdowns"}),Rn("p",{className:lo(["text-zinc-600 text-xs"]),children:"Enable audio alerts to hear a delightful ding every time a large slowdown is recorded"}),Rn("button",{onClick:()=>{if(e.audioNotificationsOptions.enabled)return void t(e=>{var t,n;return"closed"!==(null==(t=e.audioNotificationsOptions.audioContext)?void 0:t.state)&&(null==(n=e.audioNotificationsOptions.audioContext)||n.close()),localStorage.setItem("react-scan-notifications-audio","false"),{...e,audioNotificationsOptions:{audioContext:null,enabled:!1}}});localStorage.setItem("react-scan-notifications-audio","true");const n=new AudioContext;nn(n),t(e=>({...e,audioNotificationsOptions:{enabled:!0,audioContext:n}}))},className:lo(["px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-sm w-full"," text-sm flex items-center gap-x-2 justify-center"]),children:e.audioNotificationsOptions.enabled?Rn(me,{children:Rn("span",{className:"flex items-center gap-x-1",children:"Disable audio alerts"})}):Rn(me,{children:Rn("span",{className:"flex items-center gap-x-1",children:"Enable audio alerts"})})})]})})]});switch(e.route){case"render-visualization":return Rn(Qd,{children:Rn(Yd,{selectedEvent:e.selectedEvent})});case"render-explanation":if(!e.selectedFiber)throw new Error("Invariant: must have selected fiber when viewing render explanation");return Rn(Qd,{children:Rn(Kd,{selectedFiber:e.selectedFiber,selectedEvent:e.selectedEvent})});case"other-visualization":return Rn(Qd,{children:Rn("div",{className:lo(["flex w-full h-full flex-col overflow-y-auto"]),id:"overview-scroll-container",children:Rn($d,{selectedEvent:e.selectedEvent})})});case"optimize":return Rn(Qd,{children:Rn(Md,{selectedEvent:e.selectedEvent})})}e.route},Qd=({children:e})=>{const{notificationState:t}=fd();if(!t.selectedEvent)throw new Error("Invariant: d must have selected event when viewing render explanation");return Rn("div",{className:lo(["w-full h-full flex flex-col gap-y-2"]),children:[Rn("div",{className:lo(["h-[50px] w-full"]),children:Rn(Ed,{selectedEvent:t.selectedEvent})}),Rn("div",{className:lo(["h-calc(100%-50px) flex flex-col overflow-y-auto px-3"]),children:e})]})},eu=({selectedEvent:e})=>{const t=md(e);switch(e.kind){case"interaction":return Rn("div",{className:lo(["w-full flex border-b border-[#27272A] min-h-[48px]"]),children:Rn("div",{className:lo(["min-w-fit w-full justify-start flex items-center border-r border-[#27272A] pl-5 pr-2 text-sm gap-x-4"]),children:[Rn("div",{className:lo(["flex items-center gap-x-2 "]),children:[Rn("span",{className:lo(["text-[#5a5a5a] mr-0.5"]),children:"click"===e.type?"Clicked ":"Typed in "}),Rn("span",{children:pd(e.componentPath)}),Rn("div",{className:lo(["w-fit flex items-center justify-center h-fit text-white px-1 rounded-sm font-semibold text-[10px] whitespace-nowrap","low"===t&&"bg-green-500/50","needs-improvement"===t&&"bg-[#b77116]","high"===t&&"bg-[#b94040]"]),children:[hd(e.timing).toFixed(0),"ms processing time"]})]}),Rn("div",{className:lo(["flex items-center gap-x-2 justify-end ml-auto"]),children:Rn("div",{className:lo(["p-2 flex justify-center items-center border-[#27272A]"]),children:Rn("button",{onClick:()=>{_o.value={view:"none"}},title:"Close",children:Rn(bd,{size:18,className:"text-[#6F6F78]"})})})})]})});case"dropped-frames":return Rn("div",{className:lo(["w-full flex border-b border-[#27272A] min-h-[48px]"]),children:Rn("div",{className:lo(["min-w-fit w-full justify-start flex items-center border-r border-[#27272A] pl-5 pr-2 text-sm gap-x-4"]),children:[Rn("div",{className:lo(["flex items-center gap-x-2 "]),children:["FPS Drop",Rn("div",{className:lo(["w-fit flex items-center justify-center h-fit text-white px-1 rounded-sm font-semibold text-[10px] whitespace-nowrap","low"===t&&"bg-green-500/50","needs-improvement"===t&&"bg-[#b77116]","high"===t&&"bg-[#b94040]"]),children:["dropped to ",e.fps," FPS"]})]}),Rn("div",{className:lo(["flex items-center gap-x-2 w-2/4 justify-end ml-auto"]),children:Rn("div",{className:lo(["p-2 flex justify-center items-center border-[#27272A]"]),children:Rn("button",{onClick:()=>{_o.value={view:"none"}},children:Rn(bd,{size:18,className:"text-[#6F6F78]"})})})})]})})}},tu=({item:e,shouldFlash:t})=>{var n,r;const[o,i]=Ge(!1),a=e.events.map(md).reduce((e,t)=>{switch(t){case"high":return"high";case"needs-improvement":return"high"===e?"high":"needs-improvement";case"low":return e}},"low"),s=(({flashingItemsCount:e,totalEvents:t})=>{const[n,r]=Ge(!1),o=Xe(0),i=Xe(0);return Je(()=>{if(o.current>=t)return;const e=Date.now()-i.current;if(e>=250){r(!1);const e=setTimeout(()=>{o.current=t,i.current=Date.now(),r(!0),setTimeout(()=>{r(!1)},2e3)},50);return()=>clearTimeout(e)}{const n=setTimeout(()=>{r(!1),setTimeout(()=>{o.current=t,i.current=Date.now(),r(!0),setTimeout(()=>{r(!1)},2e3)},50)},250-e);return()=>clearTimeout(n)}},[e]),n})({flashingItemsCount:e.events.reduce((e,n)=>t(n.id)?e+1:e,0),totalEvents:e.events.length});return Rn("div",{className:lo(["flex flex-col gap-y-0.5"]),children:[Rn("button",{onClick:()=>i(e=>!e),className:lo(["pl-2 py-1.5 text-sm flex items-center rounded-sm hover:bg-[#18181B] relative overflow-hidden",s&&!o&&"after:absolute after:inset-0 after:bg-purple-500/30 after:animate-[fadeOut_1s_ease-out_forwards]"]),children:[Rn("div",{className:lo(["w-4/5 flex items-center justify-start h-full text-xs truncate gap-x-1.5"]),children:[Rn("span",{className:lo(["min-w-fit"]),children:Rn(vd,{className:lo(["text-[#A1A1AA] transition-transform",o?"rotate-90":""]),size:14},`chevron-${e.timestamp}`)}),Rn("span",{className:lo(["text-xs"]),children:"collapsed-frame-drops"===e.kind?"FPS Drops":pd(null!=(r=null==(n=e.events.at(0))?void 0:n.componentPath)?r:[])})]}),Rn("div",{className:lo(["ml-auto min-w-fit flex justify-end items-center"]),children:Rn("div",{style:{lineHeight:"10px"},className:lo(["w-fit flex items-center text-[10px] justify-center h-full text-white px-1 py-1 rounded-sm font-semibold","low"===a&&"bg-green-500/60","needs-improvement"===a&&"bg-[#b77116] text-[10px]","high"===a&&"bg-[#b94040]"]),children:["x",e.events.length]})})]}),o&&Rn(nu,{children:e.events.toSorted((e,t)=>t.timestamp-e.timestamp).map(e=>Rn(ru,{event:e,shouldFlash:t(e.id)}))})]})},nu=({children:e})=>Rn("div",{className:"relative pl-6 flex flex-col gap-y-1",children:[Rn("div",{className:"absolute left-3 top-0 bottom-0 w-px bg-[#27272A]"}),e]}),ru=({event:e,shouldFlash:t})=>{var n,r;const{notificationState:o,setNotificationState:i}=fd(),a=md(e),s=(({shouldFlash:e})=>{const[t,n]=Ge(e);return Je(()=>{if(e){n(!0);const e=setTimeout(()=>{n(!1)},1e3);return()=>clearTimeout(e)}},[e]),t})({shouldFlash:t});switch(e.kind){case"interaction":return Rn("button",{onClick:()=>{i(t=>({...t,selectedEvent:e,route:"render-visualization",selectedFiber:null}))},className:lo(["pl-2 py-1.5 text-sm flex w-full items-center rounded-sm hover:bg-[#18181B] relative overflow-hidden",e.id===(null==(n=o.selectedEvent)?void 0:n.id)&&"bg-[#18181B]",s&&"after:absolute after:inset-0 after:bg-purple-500/30 after:animate-[fadeOut_1s_ease-out_forwards]"]),children:[Rn("div",{className:lo(["w-4/5 flex items-center justify-start h-full gap-x-1.5"]),children:[Rn("span",{className:lo(["min-w-fit text-xs"]),children:Oc(()=>{switch(e.type){case"click":return Rn(_d,{size:14});case"keyboard":return Rn(Nd,{size:14})}})}),Rn("span",{className:lo(["text-xs pr-1 truncate"]),children:pd(e.componentPath)})]}),Rn("div",{className:lo([" min-w-fit flex justify-end items-center ml-auto"]),children:Rn("div",{style:{lineHeight:"10px"},className:lo(["gap-x-0.5 w-fit flex items-end justify-center h-full text-white px-1 py-1 rounded-sm font-semibold text-[10px]","low"===a&&"bg-green-500/50","needs-improvement"===a&&"bg-[#b77116] text-[10px]","high"===a&&"bg-[#b94040]"]),children:Rn("div",{style:{lineHeight:"10px"},className:lo(["text-[10px] text-white flex items-end"]),children:[hd(e.timing).toFixed(0),"ms"]})})})]});case"dropped-frames":return Rn("button",{onClick:()=>{i(t=>({...t,selectedEvent:e,route:"render-visualization",selectedFiber:null}))},className:lo(["pl-2 py-1.5 w-full text-sm flex items-center rounded-sm hover:bg-[#18181B] relative overflow-hidden",e.id===(null==(r=o.selectedEvent)?void 0:r.id)&&"bg-[#18181B]",s&&"after:absolute after:inset-0 after:bg-purple-500/30 after:animate-[fadeOut_1s_ease-out_forwards]"]),children:[Rn("div",{className:lo(["w-4/5 flex items-center justify-start h-full text-xs truncate"]),children:[Rn(Cd,{size:14,className:"mr-1.5"})," FPS Drop"]}),Rn("div",{className:lo([" min-w-fit flex justify-end items-center ml-auto"]),children:Rn("div",{style:{lineHeight:"10px"},className:lo(["w-fit flex items-center justify-center h-full text-white px-1 py-1 rounded-sm text-[10px] font-bold","low"===a&&"bg-green-500/60","needs-improvement"===a&&"bg-[#b77116] text-[10px]","high"===a&&"bg-[#b94040]"]),children:[e.fps," FPS"]})})]})}},ou=(e=150)=>{const{notificationState:t}=fd(),[n,r]=Ge(t.events);return Je(()=>{setTimeout(()=>{r(t.events)},e)},[t.events]),[n,r]},iu=()=>{const{notificationState:e,setNotificationState:t}=fd(),n=(e=>{const t=Xe([]),[n,r]=Ge(new Set),o=Xe(!0);return Je(()=>{if(o.current)return o.current=!1,void(t.current=e);const n=new Set(e.map(e=>e.id)),i=new Set(t.current.map(e=>e.id)),a=new Set;n.forEach(e=>{i.has(e)||a.add(e)}),a.size>0&&(r(a),setTimeout(()=>{r(new Set)},2e3)),t.current=e},[e]),e=>n.has(e)})(e.events),[r,o]=ou(),i=(a=r,a.reduce((e,t)=>{const n=e.at(-1);if(!n)return[{kind:"single",event:t,timestamp:t.timestamp}];switch(n.kind){case"collapsed-keyboard":return"interaction"===t.kind&&"keyboard"===t.type&&t.componentPath.join("-")===n.events[0].componentPath.join("-")?[...e.filter(e=>e!==n),{kind:"collapsed-keyboard",events:[...n.events,t],timestamp:Math.max(...[...n.events,t].map(e=>e.timestamp))}]:[...e,{kind:"single",event:t,timestamp:t.timestamp}];case"single":return"interaction"===n.event.kind&&"keyboard"===n.event.type&&"interaction"===t.kind&&"keyboard"===t.type&&n.event.componentPath.join("-")===t.componentPath.join("-")?[...e.filter(e=>e!==n),{kind:"collapsed-keyboard",events:[n.event,t],timestamp:Math.max(n.event.timestamp,t.timestamp)}]:"dropped-frames"===n.event.kind&&"dropped-frames"===t.kind?[...e.filter(e=>e!==n),{kind:"collapsed-frame-drops",events:[n.event,t],timestamp:Math.max(n.event.timestamp,t.timestamp)}]:[...e,{kind:"single",event:t,timestamp:t.timestamp}];case"collapsed-frame-drops":return"dropped-frames"===t.kind?[...e.filter(e=>e!==n),{kind:"collapsed-frame-drops",events:[...n.events,t],timestamp:Math.max(...[...n.events,t].map(e=>e.timestamp))}]:[...e,{kind:"single",event:t,timestamp:t.timestamp}]}},[])).toSorted((e,t)=>t.timestamp-e.timestamp);var a;return Rn("div",{className:lo(["w-full h-full gap-y-2 flex flex-col border-r border-[#27272A] overflow-y-auto"]),children:[Rn("div",{className:lo(["text-sm text-[#65656D] pl-3 pr-1 w-full flex items-center justify-between"]),children:[Rn("span",{children:"History"}),Rn(Td,{wrapperProps:{className:"h-full flex items-center justify-center ml-auto"},triggerContent:Rn("button",{className:lo(["hover:bg-[#18181B] rounded-full p-2"]),title:"Clear all events",onClick:()=>{id.getState().actions.clear(),t(e=>({...e,selectedEvent:null,selectedFiber:null,route:"other-visualization"===e.route?"other-visualization":"render-visualization"})),o([])},children:Rn(Sd,{className:lo([""]),size:16})}),children:Rn("div",{className:lo(["w-full flex justify-center"]),children:"Clear all events"})})]}),Rn("div",{className:lo(["flex flex-col px-1 gap-y-1"]),children:[0===i.length&&Rn("div",{className:lo(["flex items-center justify-center text-zinc-500 text-sm py-4"]),children:"No Events"}),i.map(e=>Oc(()=>{switch(e.kind){case"collapsed-keyboard":case"collapsed-frame-drops":return Rn(tu,{shouldFlash:n,item:e});case"single":return Rn(ru,{event:e.event,shouldFlash:n(e.event.id)},e.event.id)}}))]})]})},au=()=>{const e=ad(),t=[];return(e=>{Je(()=>{const t=setInterval(()=>{e.forEach(e=>{e.groupedFiberRenders&&e.groupedFiberRenders.forEach(e=>{if(e.deletedAll)return;if(!e.elements||0===e.elements.length)return void(e.deletedAll=!0);const t=e.elements.length;e.elements=e.elements.filter(e=>e&&e.isConnected),0===e.elements.length&&t>0&&(e.deletedAll=!0)})})},5e3);return()=>{clearInterval(t)}},[e])})(t),e.state.events.forEach(e=>{const n=(e=>Object.values(e).map(e=>({id:tn(),totalTime:e.nodeInfo.reduce((e,t)=>e+t.selfTime,0),count:e.nodeInfo.length,name:e.nodeInfo[0].name,deletedAll:!1,parents:e.parents,hasMemoCache:e.hasMemoCache,wasFiberRenderMount:e.wasFiberRenderMount,elements:e.nodeInfo.map(e=>e.element),changes:{context:e.changes.fiberContext.current.filter(t=>e.changes.fiberContext.changesCounts.get(t.name)).map(t=>{var n;return{name:String(t.name),count:null!=(n=e.changes.fiberContext.changesCounts.get(t.name))?n:0}}),props:e.changes.fiberProps.current.filter(t=>e.changes.fiberProps.changesCounts.get(t.name)).map(t=>{var n;return{name:String(t.name),count:null!=(n=e.changes.fiberProps.changesCounts.get(t.name))?n:0}}),state:e.changes.fiberState.current.filter(t=>e.changes.fiberState.changesCounts.get(Number(t.name))).map(t=>{var n;return{index:t.name,count:null!=(n=e.changes.fiberState.changesCounts.get(Number(t.name)))?n:0}})}})))("interaction"===e.kind?e.data.meta.detailedTiming.fiberRenders:e.data.meta.fiberRenders),r=n.reduce((e,t)=>e+t.totalTime,0);switch(e.kind){case"interaction":{const{commitEnd:o,jsEndDetail:i,interactionStartDetail:a,rafStart:s}=e.data.meta.detailedTiming,l=Math.max(0,i-a-r),c=Math.max(e.data.meta.latency-(o-a),0);return void t.push({componentPath:e.data.meta.detailedTiming.componentPath,groupedFiberRenders:n,id:e.id,kind:"interaction",memory:null,timestamp:e.data.startAt,type:"keyboard"===e.data.meta.detailedTiming.interactionType?"keyboard":"click",timing:{renderTime:r,kind:"interaction",otherJSTime:l,framePreparation:s-i,frameConstruction:o-s,frameDraw:c}})}case"long-render":return void t.push({kind:"dropped-frames",id:e.id,memory:null,timing:{kind:"dropped-frames",renderTime:r,otherTime:e.data.meta.latency},groupedFiberRenders:n,timestamp:e.data.startAt,fps:e.data.meta.fps})}}),t},su=()=>{const{notificationState:e,setNotificationState:t}=fd(),n=Xe(null),r=Xe(null),o=Xe(0),[i]=ou(),a=i.filter(e=>"high"===md(e)).length;return Je(()=>{const e=localStorage.getItem("react-scan-notifications-audio");if("false"!==e&&"true"!==e)return void localStorage.setItem("react-scan-notifications-audio","false");"false"!==e&&t(e=>e.audioNotificationsOptions.enabled?e:{...e,audioNotificationsOptions:{enabled:!0,audioContext:new AudioContext}})},[]),Je(()=>{const{audioNotificationsOptions:t}=e;if(!t.enabled)return;if(0===a)return;if(n.current&&n.current>=a)return;r.current&&clearTimeout(r.current);const i=Date.now()-o.current,s=Math.max(0,1e3-i);r.current=setTimeout(()=>{nn(t.audioContext),n.current=a,o.current=Date.now(),r.current=null},s)},[a]),Je(()=>{0===a&&(n.current=null)},[a]),Je(()=>()=>{r.current&&clearTimeout(r.current)},[]),null},lu=un((e,t)=>{var n;const r=au(),[o,i]=Ge({detailsExpanded:!1,events:r,filterBy:"latest",moreInfoExpanded:!1,route:"render-visualization",selectedEvent:null!=(n=r.toSorted((e,t)=>e.timestamp-t.timestamp).at(-1))?n:null,selectedFiber:null,routeMessage:null,audioNotificationsOptions:{enabled:!1,audioContext:null}});return o.events=r,Rn(gd.Provider,{value:{notificationState:o,setNotificationState:i,setRoute:({route:e,routeMessage:t})=>{i(n=>{const r={...n,route:e,routeMessage:t};switch(e){case"render-visualization":case"optimize":case"other-visualization":return Jd(),{...r,selectedFiber:null};case"render-explanation":return Jd(),r}})}},children:[Rn(su,{}),Rn(cu,{ref:t})]})}),cu=un((e,t)=>{var n;const{notificationState:r}=fd();return Rn("div",{ref:t,className:lo(["h-full w-full flex flex-col"]),children:[r.selectedEvent&&Rn("div",{className:lo(["w-full h-[48px] flex flex-col",r.moreInfoExpanded&&"h-[235px]",r.moreInfoExpanded&&"dropped-frames"===r.selectedEvent.kind&&"h-[150px]"]),children:[Rn(eu,{selectedEvent:r.selectedEvent}),r.moreInfoExpanded&&Rn(du,{})]}),Rn("div",{className:lo(["flex ",r.selectedEvent?"h-[calc(100%-48px)]":"h-full",r.moreInfoExpanded&&"h-[calc(100%-200px)]",r.moreInfoExpanded&&"dropped-frames"===(null==(n=r.selectedEvent)?void 0:n.kind)&&"h-[calc(100%-150px)]"]),children:[Rn("div",{className:lo(["h-full min-w-[200px]"]),children:Rn(iu,{})}),Rn("div",{className:lo(["w-[calc(100%-200px)] h-full overflow-y-auto"]),children:Rn(Zd,{})})]})]})}),du=()=>{const{notificationState:e}=fd();if(!e.selectedEvent)throw new Error("Invariant must have selected event for more info");const t=e.selectedEvent;return Rn("div",{className:lo(["px-4 py-2 border-b border-[#27272A] bg-[#18181B]/50 h-[calc(100%-40px)]","dropped-frames"===t.kind&&"h-[calc(100%-25px)]"]),children:Rn("div",{className:lo(["flex flex-col gap-y-4 h-full"]),children:Oc(()=>{switch(t.kind){case"interaction":return Rn(me,{children:[Rn("div",{className:lo(["flex items-center gap-x-3"]),children:[Rn("span",{className:"text-[#6F6F78] text-xs font-medium",children:"click"===t.type?"Clicked component location":"Typed in component location"}),Rn("div",{className:"font-mono text-[#E4E4E7] flex items-center bg-[#27272A] pl-2 py-1 rounded-sm overflow-x-auto",children:t.componentPath.toReversed().map((e,n)=>Rn(me,{children:[Rn("span",{style:{lineHeight:"14px"},className:"text-[10px] whitespace-nowrap",children:e},e),n{var e;const t=au(),[n,r]=Ge(t);Je(()=>{const e=setTimeout(()=>{r(t)},600);return()=>{clearTimeout(e)}},[t]);const o=Lu.inspectState,i="inspecting"===o.value.kind,a="focused"===o.value.kind,[s,l]=Ge([]),c=Ze(()=>{switch(Lu.inspectState.value.kind){case"inspecting":return _o.value={view:"none"},void(Lu.inspectState.value={kind:"inspect-off"});case"focused":return _o.value={view:"inspector"},void(Lu.inspectState.value={kind:"inspecting",hoveredDomElement:null});case"inspect-off":return _o.value={view:"none"},void(Lu.inspectState.value={kind:"inspecting",hoveredDomElement:null});case"uninitialized":return}},[]),d=Ze(e=>{if(e.preventDefault(),e.stopPropagation(),!Pu.instrumentation)return;const t=!Pu.instrumentation.isPaused.value;Pu.instrumentation.isPaused.value=t;const n=uo("react-scan-options");po("react-scan-options",{...n,enabled:!t})},[]);Jt(()=>{"uninitialized"===Lu.inspectState.value.kind&&(Lu.inspectState.value={kind:"inspect-off"})});let u=null,p="#999";return i?(u=Rn(On,{name:"icon-inspect"}),p="#8e61e3"):a?(u=Rn(On,{name:"icon-focus"}),p="#8e61e3"):(u=Rn(On,{name:"icon-inspect"}),p="#999"),Ye(()=>{if("notifications"!==_o.value.view)return;const e=new Set(t.map(e=>e.id));l([...e.values()])},[t.length,_o.value.view]),Rn("div",{className:"flex max-h-9 min-h-9 flex-1 items-stretch overflow-hidden",children:[Rn("div",{className:"h-full flex items-center min-w-fit",children:Rn("button",{type:"button",id:"react-scan-inspect-element",title:"Inspect element",onClick:c,className:"button flex items-center justify-center h-full w-full pl-3 pr-2.5",style:{color:p},children:u})}),Rn("div",{className:"h-full flex items-center justify-center",children:Rn("button",{type:"button",id:"react-scan-notifications",title:"Notifications",onClick:()=>{switch("inspect-off"!==Lu.inspectState.value.kind&&(Lu.inspectState.value={kind:"inspect-off"}),_o.value.view){case"inspector":{Lu.inspectState.value={kind:"inspect-off"};const e=new Set(t.map(e=>e.id));return l([...e.values()]),void(_o.value={view:"notifications"})}case"notifications":return void(_o.value={view:"none"});case"none":{const e=new Set(t.map(e=>e.id));return l([...e.values()]),void(_o.value={view:"notifications"})}}},className:"button flex items-center justify-center h-full pl-2.5 pr-2.5",style:{color:p},children:Rn(wd,{events:n.filter(e=>!s.includes(e.id)).map(e=>"high"===md(e)),size:16,className:lo(["text-[#999]","notifications"===_o.value.view&&"text-[#8E61E3]"])})})}),Rn($c,{checked:!(null==(e=Pu.instrumentation)?void 0:e.isPaused.value),onChange:d,className:"place-self-center",title:"Outline Re-renders"}),Pu.options.value.showFPS&&Rn(Rc,{})]})}),pu=Et(()=>"inspecting"===Lu.inspectState.value.kind),hu=Et(()=>lo("relative","flex-1","flex flex-col","rounded-t-lg","overflow-hidden","opacity-100","transition-[opacity]",pu.value&&"opacity-0 duration-0 delay-0")),mu=Et(()=>"inspector"===_o.value.view),fu=Et(()=>"notifications"===_o.value.view),gu=()=>Rn("div",{className:lo("flex flex-1 flex-col","overflow-hidden z-10","rounded-lg","bg-black","opacity-100","transition-[border-radius]","peer-hover/left:rounded-l-none","peer-hover/right:rounded-r-none","peer-hover/top:rounded-t-none","peer-hover/bottom:rounded-b-none"),children:[Rn("div",{className:hu,children:[Rn(Mc,{}),Rn("div",{className:lo("relative","flex-1 flex","text-white","bg-[#0A0A0A]","transition-opacity delay-150","overflow-hidden","border-b border-[#222]"),children:[Rn(vu,{isOpen:mu,children:Rn(fi,{})}),Rn(vu,{isOpen:fu,children:Rn(lu,{})})]})]}),Rn(uu,{})]}),vu=({isOpen:e,children:t})=>Rn("div",{className:lo("flex-1","opacity-0","overflow-y-auto overflow-x-hidden","transition-opacity delay-0","pointer-events-none",e.value&&"opacity-100 delay-150 pointer-events-auto"),children:Rn("div",{className:"absolute inset-0 flex",children:t})}),wu=(e,t,n)=>e+(t-e)*n,bu={frameInterval:1e3/60,speeds:{fast:.51,slow:.1,off:0}},xu=ie&&window.devicePixelRatio||1,yu=()=>{const e=Xe(null),t=Xe(null),n=Xe(null),r=Xe(null),o=Xe(null),i=Xe(0),a=Xe(),s=Xe(new Map),l=Xe(!1),c=Xe(0),d=(e,t,n,o)=>{var i;if(!o)return;const a=null!=(i=(null==o?void 0:o.type)&&J(o.type))?i:"Unknown";e.save(),e.font="12px system-ui, -apple-system, sans-serif";const s="locked"===n?14:0,l="locked"===n?6:0,c=e.measureText(a).width+16+s+l,d=t.left,u=t.top-24-4;if(e.fillStyle="rgb(37, 37, 38, .75)",e.beginPath(),e.roundRect(d,u,c,24,3),e.fill(),"locked"===n){const t=d+8,n=u+(24-s)/2+2;((e,t,n,r)=>{e.save(),e.strokeStyle="white",e.fillStyle="white",e.lineWidth=1.5;const o=.6*r,i=.5*r,a=t+(r-o)/2,s=n;e.beginPath(),e.arc(a+o/2,s+i/2,o/2,Math.PI,0,!1),e.stroke();const l=.8*r,c=.5*r,d=t+(r-l)/2,u=n+i/2;e.fillRect(d,u,l,c),e.restore()})(e,t,n,s),r.current={x:t,y:n,width:s,height:s}}else r.current=null;e.fillStyle="white",e.textBaseline="middle";const p=d+8+("locked"===n?s+l:0);e.fillText(a,p,u+12),e.restore()},u=(e,t,r,o)=>{if(!n.current)return;const i=n.current;t.clearRect(0,0,e.width,e.height),t.strokeStyle="rgba(142, 97, 227, 0.5)",t.fillStyle="rgba(173, 97, 230, 0.10)","locked"===r?t.setLineDash([]):t.setLineDash([4]),t.lineWidth=1,t.fillRect(i.left,i.top,i.width,i.height),t.strokeRect(i.left,i.top,i.width,i.height),d(t,i,r,o)},p=(e,t,r,o,s)=>{if(t.save(),!n.current)return n.current=r,u(e,t,o,s),void t.restore();((e,t,r,o,s)=>{var l;const d=Pu.options.value.animationSpeed,p=null!=(l=bu.speeds[d])?l:bu.speeds.off,h=a=>{a-c.current.1||Math.abs(n.current.top-r.top)>.1||Math.abs(n.current.width-r.width)>.1||Math.abs(n.current.height-r.height)>.1?i.current=requestAnimationFrame(h):(n.current=r,u(e,t,o,s),cancelAnimationFrame(i.current),t.restore())):cancelAnimationFrame(i.current))};cancelAnimationFrame(i.current),clearTimeout(a.current),i.current=requestAnimationFrame(h),a.current=setTimeout(()=>{cancelAnimationFrame(i.current),n.current=r,u(e,t,o,s),t.restore()},1e3)})(e,t,r,o,s)},h=async(e,t,n,r)=>{if(!e||!t||!n)return;const{parentCompositeFiber:o}=yi(e),i=await(async e=>{const t=wi(e);if(!t)return null;const n=vi(t);return n?await new Promise(e=>{const t=new IntersectionObserver(n=>{var r,o;t.disconnect(),e(null!=(o=null==(r=n[0])?void 0:r.boundingClientRect)?o:null)});t.observe(n)}):null})(e);o&&i&&p(t,n,i,r,o)},m=t=>{if(!e.current||l.current)return;const i=a=>{e.current&&"opacity"===a.propertyName&&l.current&&(e.current.removeEventListener("transitionend",i),(e=>{const t=e.getContext("2d");t&&t.clearRect(0,0,e.width,e.height),n.current=null,r.current=null,o.current=null,e.classList.remove("fade-in"),l.current=!1})(e.current),null==t||t())},a=s.current.get("fade-out");a&&(a(),s.current.delete("fade-out")),e.current.addEventListener("transitionend",i),s.current.set("fade-out",()=>{var t;null==(t=e.current)||t.removeEventListener("transitionend",i)}),l.current=!0,e.current.classList.remove("fade-in"),requestAnimationFrame(()=>{var t;null==(t=e.current)||t.classList.add("fade-out")})},f=()=>{e.current&&(l.current=!1,e.current.classList.remove("fade-out"),requestAnimationFrame(()=>{var t;null==(t=e.current)||t.classList.add("fade-in")}))},g=co(r=>{var i,s;if("inspecting"!==Lu.inspectState.peek().kind||!t.current)return;t.current.style.pointerEvents="none";const c=document.elementFromPoint(null!=(i=null==r?void 0:r.clientX)?i:0,null!=(s=null==r?void 0:r.clientY)?s:0);if(t.current.style.removeProperty("pointer-events"),clearTimeout(a.current),c&&c!==e.current){const{parentCompositeFiber:e}=yi(c);if(e){const t=Si(e);if(t)return void(e=>{e!==o.current&&(o.current=e,Ni.has(e.tagName)?m():f(),Lu.inspectState.value={kind:"inspecting",hoveredDomElement:e})})(t)}}n.current&&e.current&&!l.current&&m()},32),v=(e,t)=>{const n=r.current;if(!n)return!1;const o=t.getBoundingClientRect(),i=t.width/o.width,a=t.height/o.height,s=(e.clientX-o.left)*i,l=(e.clientY-o.top)*a,c=s/xu,d=l/xu;return c>=n.x&&c<=n.x+n.width&&d>=n.y&&d<=n.y+n.height},w=n=>{if(n.__reactScanSyntheticEvent)return;const r=Lu.inspectState.peek(),i=e.current;return i&&t.current?v(n,i)?(n.preventDefault(),n.stopPropagation(),void(e=>{"focused"===e.kind&&(Lu.inspectState.value={kind:"inspecting",hoveredDomElement:e.focusedDomElement})})(r)):void("inspecting"===r.kind&&(e=>{var t,n;const r=["react-scan-inspect-element","react-scan-power"];if(e.target instanceof HTMLElement&&r.includes(e.target.id))return;const i=null==(t=o.current)?void 0:t.tagName;if(i&&Ni.has(i))return;e.preventDefault(),e.stopPropagation();const a=null!=(n=o.current)?n:document.elementFromPoint(e.clientX,e.clientY);if(!a)return;const s=e.composedPath().at(0);if(s instanceof HTMLElement&&r.includes(s.id)){const t=new MouseEvent(e.type,e);return t.__reactScanSyntheticEvent=!0,void s.dispatchEvent(t)}const{parentCompositeFiber:l}=yi(a);if(!l)return;const c=Si(l);if(!c)return o.current=null,void(Lu.inspectState.value={kind:"inspect-off"});Lu.inspectState.value={kind:"focused",focusedDomElement:c,fiber:l}})(n)):void 0},b=t=>{var r;if("Escape"!==t.key)return;const i=Lu.inspectState.peek();if(e.current&&"react-scan-root"!==(null==(r=document.activeElement)?void 0:r.id)&&(_o.value={view:"none"},"focused"===i.kind||"inspecting"===i.kind))switch(t.preventDefault(),t.stopPropagation(),i.kind){case"focused":f(),n.current=null,o.current=i.focusedDomElement,Lu.inspectState.value={kind:"inspecting",hoveredDomElement:i.focusedDomElement};break;case"inspecting":m(()=>{wo.value=!1,Lu.inspectState.value={kind:"inspect-off"}})}},x=(e,t)=>{const n=e.getBoundingClientRect();e.width=n.width*xu,e.height=n.height*xu,t.scale(xu,xu),t.save()},y=()=>{const t=Lu.inspectState.peek(),r=e.current;if(!r)return;const o=null==r?void 0:r.getContext("2d");o&&(cancelAnimationFrame(i.current),clearTimeout(a.current),x(r,o),n.current=null,"focused"===t.kind&&t.focusedDomElement?h(t.focusedDomElement,r,o,"locked"):"inspecting"===t.kind&&t.hoveredDomElement&&h(t.hoveredDomElement,r,o,"inspecting"))},k=t=>{const n=Lu.inspectState.peek(),r=e.current;r&&("inspecting"===n.kind||v(t,r))&&(t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation())};return Je(()=>{const r=e.current;if(!r)return;const l=null==r?void 0:r.getContext("2d");if(!l)return;x(r,l);const c=Lu.inspectState.subscribe(e=>{((e,r,a)=>{var l;let c;switch(null==(l=s.current.get(e.kind))||l(),t.current&&"inspecting"!==e.kind&&(t.current.style.pointerEvents="none"),i.current&&cancelAnimationFrame(i.current),e.kind){case"inspect-off":return void m();case"inspecting":h(e.hoveredDomElement,r,a,"inspecting");break;case"focused":if(!e.focusedDomElement)return;o.current!==e.focusedDomElement&&(o.current=e.focusedDomElement),_o.value={view:"inspector"},h(e.focusedDomElement,r,a,"locked"),c=Lu.lastReportTime.subscribe(()=>{if(i.current&&n.current){const{parentCompositeFiber:t}=yi(e.focusedDomElement);t&&h(e.focusedDomElement,r,a,"locked")}}),c&&s.current.set(e.kind,c)}})(e,r,l)});return window.addEventListener("scroll",y,{passive:!0}),window.addEventListener("resize",y,{passive:!0}),document.addEventListener("pointermove",g,{passive:!0,capture:!0}),document.addEventListener("pointerdown",k,{capture:!0}),document.addEventListener("click",w,{capture:!0}),document.addEventListener("keydown",b,{capture:!0}),()=>{(()=>{for(const e of s.current.values())null==e||e()})(),c(),window.removeEventListener("scroll",y),window.removeEventListener("resize",y),document.removeEventListener("pointermove",g,{capture:!0}),document.removeEventListener("click",w,{capture:!0}),document.removeEventListener("pointerdown",k,{capture:!0}),document.removeEventListener("keydown",b,{capture:!0}),i.current&&cancelAnimationFrame(i.current),clearTimeout(a.current)}},[]),Rn(me,{children:[Rn("div",{ref:t,className:lo("fixed top-0 left-0 w-screen h-screen","z-[214748365]"),style:{pointerEvents:"none"}}),Rn("canvas",{ref:e,dir:"ltr",className:lo("react-scan-inspector-overlay","fixed top-0 left-0 w-screen h-screen","pointer-events-none","z-[214748367]")})]})},ku=class{constructor(e,t,r){n(this,"width",e),n(this,"height",t),n(this,"safeArea",r),n(this,"maxWidth"),n(this,"maxHeight"),this.maxWidth=e-r.left-r.right,this.maxHeight=t-r.top-r.bottom}rightEdge(e){return this.width-e-this.safeArea.right}bottomEdge(e){return this.height-e-this.safeArea.bottom}isFullWidth(e){return e>=this.maxWidth}isFullHeight(e){return e>=this.maxHeight}},_u=()=>{const e=window.innerWidth,t=window.innerHeight,n=vo();return Gd&&Gd.width===e&&Gd.height===t&&(r=Gd.safeArea,o=n,r.top===o.top&&r.right===o.right&&r.bottom===o.bottom&&r.left===o.left)?Gd:Gd=new ku(e,t,n);var r,o},Nu=(e,t,n)=>{const r="rtl"===getComputedStyle(document.body).direction,o=window.innerWidth,i=window.innerHeight,a=vo(),s=t===jn,l=s?t:Math.min(t,o-a.left-a.right),c=s?n:Math.min(n,i-a.top-a.bottom);let d,u,p=a.left,h=o-l-a.right,m=a.top,f=i-c-a.bottom;const g=-a.right,v=-(o-l-a.left);switch(e){case"top-right":d=r?g:h,u=m;break;case"bottom-right":d=r?g:h,u=f;break;case"bottom-left":d=r?v:p,u=f;break;case"top-left":d=r?v:p,u=m;break;default:d=p,u=m}return s&&(d=r?Math.min(g,Math.max(d,v)):Math.max(p,Math.min(d,h)),u=Math.max(m,Math.min(u,f))),{x:d,y:u}},Su=(e,t,n)=>{const r=n?jn:Pn,o=n?_u().maxWidth:_u().maxHeight,i=e+t;return Math.min(Math.max(r,i),o)},Cu=({position:e})=>{const t=Xe(null),n=Xe(null),r=Xe(null),o=Xe(null);Je(()=>{const i=t.current;if(!i)return;const a=()=>{i.classList.remove("pointer-events-none");const t="focused"===Lu.inspectState.value.kind,n="none"!==_o.value.view,r=(t||n)&&((e,t,n,r)=>!(!n||!r)||(n||r?n?e!==t.split("-")[0]:!!r&&e!==t.split("-")[1]:((e,t)=>{const[n,r]=t.split("-");return e!==n&&e!==r})(e,t)))(e,yo.value.corner,yo.value.dimensions.isFullWidth,yo.value.dimensions.isFullHeight);r?i.classList.remove("hidden","pointer-events-none","opacity-0"):i.classList.add("hidden","pointer-events-none","opacity-0")},s=yo.subscribe(e=>{null!==n.current&&null!==r.current&&null!==o.current&&e.dimensions.width===n.current&&e.dimensions.height===r.current&&e.corner===o.current||(a(),n.current=e.dimensions.width,r.current=e.dimensions.height,o.current=e.corner)}),l=Lu.inspectState.subscribe(()=>{a()});return()=>{s(),l(),n.current=null,r.current=null,o.current=null}},[]);const i=Ze(t=>{t.preventDefault(),t.stopPropagation();const n=bo.value;if(!n)return;const r=n.style,{dimensions:o}=yo.value,i=t.clientX,a=t.clientY,s=o.width,l=o.height,c=o.position;yo.value={...yo.value,dimensions:{...o,isFullWidth:!1,isFullHeight:!1,width:s,height:l,position:c}};let d=null;const u=t=>{d||(r.transition="none",d=requestAnimationFrame(()=>{const{newSize:n,newPosition:o}=((e,t,n,r,o)=>{const i="rtl"===getComputedStyle(document.body).direction,a=vo(),s=window.innerWidth-a.left-a.right,l=window.innerHeight-a.top-a.bottom;let c=t.width,d=t.height,u=n.x,p=n.y;if(i&&e.includes("right")){const e=-n.x+t.width-a.right,o=Math.min(t.width+r,e);c=Math.min(s,Math.max(jn,o)),u=n.x+(c-t.width)}if(i&&e.includes("left")){const e=window.innerWidth-n.x-a.left,o=Math.min(t.width-r,e);c=Math.min(s,Math.max(jn,o))}if(!i&&e.includes("right")){const e=window.innerWidth-n.x-a.right,o=Math.min(t.width+r,e);c=Math.min(s,Math.max(jn,o))}if(!i&&e.includes("left")){const e=n.x+t.width-a.left,o=Math.min(t.width-r,e);c=Math.min(s,Math.max(jn,o)),u=n.x-(c-t.width)}if(e.includes("bottom")){const e=window.innerHeight-n.y-a.bottom,r=Math.min(t.height+o,e);d=Math.min(l,Math.max(Pn,r))}if(e.includes("top")){const e=n.y+t.height-a.top,r=Math.min(t.height-o,e);d=Math.min(l,Math.max(Pn,r)),p=n.y-(d-t.height)}let h=a.left,m=window.innerWidth-a.right-c,f=a.top,g=window.innerHeight-a.bottom-d;const v=-a.right,w=-(window.innerWidth-c-a.left);return u=i?Math.min(v,Math.max(u,w)):Math.max(h,Math.min(u,m)),p=Math.max(f,Math.min(p,g)),{newSize:{width:c,height:d},newPosition:{x:u,y:p}}})(e,{width:s,height:l},c,t.clientX-i,t.clientY-a);r.transform=`translate3d(${o.x}px, ${o.y}px, 0)`,r.width=`${n.width}px`,r.height=`${n.height}px`;const u=Math.floor(n.width-120),p=yo.value.componentsTree.width,h=Math.min(u,Math.max(In,p));yo.value={...yo.value,dimensions:{isFullWidth:!1,isFullHeight:!1,width:n.width,height:n.height,position:o},componentsTree:{...yo.value.componentsTree,width:h}},d=null}))},p=()=>{d&&(cancelAnimationFrame(d),d=null),document.removeEventListener("pointermove",u),document.removeEventListener("pointerup",p);const{dimensions:e,corner:t}=yo.value,o=_u(),i=o.isFullWidth(e.width),a=o.isFullHeight(e.height);let s=t;(i&&a||i||a)&&(s=(e=>{const t=_u(),n={"top-left":Math.hypot(e.x,e.y),"top-right":Math.hypot(t.maxWidth-e.x,e.y),"bottom-left":Math.hypot(e.x,t.maxHeight-e.y),"bottom-right":Math.hypot(t.maxWidth-e.x,t.maxHeight-e.y)};let r="top-left";for(const e in n)n[e]{n.removeEventListener("transitionend",c)};n.addEventListener("transitionend",c),r.transform=`translate3d(${l.x}px, ${l.y}px, 0)`,yo.value={...yo.value,corner:s,dimensions:{isFullWidth:i,isFullHeight:a,width:e.width,height:e.height,position:l},lastDimensions:{isFullWidth:i,isFullHeight:a,width:e.width,height:e.height,position:l}},po(Wn,{corner:s,dimensions:yo.value.dimensions,lastDimensions:yo.value.lastDimensions,componentsTree:yo.value.componentsTree})};document.addEventListener("pointermove",u,{passive:!0}),document.addEventListener("pointerup",p)},[]),a=Ze(t=>{t.preventDefault(),t.stopPropagation();const n=bo.value;if(!n)return;const r=n.style,{dimensions:o,corner:i}=yo.value,a=_u(),s=a.isFullWidth(o.width),l=a.isFullHeight(o.height),c=s&&l,d=(s||l)&&!c;let u=o.width,p=o.height;const h=((e,t,n,r,o)=>{if(n){if("top-left"===e)return"bottom-right";if("top-right"===e)return"bottom-left";if("bottom-left"===e)return"top-right";if("bottom-right"===e)return"top-left";const[n,r]=t.split("-");if("left"===e)return`${n}-right`;if("right"===e)return`${n}-left`;if("top"===e)return`bottom-${r}`;if("bottom"===e)return`top-${r}`}if(r){if("left"===e)return`${t.split("-")[0]}-right`;if("right"===e)return`${t.split("-")[0]}-left`}if(o){if("top"===e)return`bottom-${t.split("-")[1]}`;if("bottom"===e)return`top-${t.split("-")[1]}`}return t})(e,i,c,s,l);"left"===e||"right"===e?(u=s?o.width:a.maxWidth,d&&(u=s?jn:a.maxWidth)):(p=l?o.height:a.maxHeight,d&&(p=l?Pn:a.maxHeight)),c&&("left"===e||"right"===e?u=jn:p=Pn);const m=Nu(h,u,p),f={isFullWidth:a.isFullWidth(u),isFullHeight:a.isFullHeight(p),width:u,height:p,position:m},g=Math.floor(u-jn/2),v=yo.value.componentsTree.width,w=Math.floor(.3*u),b=s?In:"left"!==e&&"right"!==e||s?Math.min(g,Math.max(In,v)):Math.min(g,Math.max(In,w));requestAnimationFrame(()=>{yo.value={corner:h,dimensions:f,lastDimensions:o,componentsTree:{...yo.value.componentsTree,width:b}},r.transition="all 0.25s cubic-bezier(0, 0, 0.2, 1)",r.width=`${u}px`,r.height=`${p}px`,r.transform=`translate3d(${m.x}px, ${m.y}px, 0)`}),po(Wn,{corner:h,dimensions:f,lastDimensions:o,componentsTree:{...yo.value.componentsTree,width:b}})},[]);return Rn("div",{ref:t,onPointerDown:i,onDblClick:a,className:lo("absolute z-50","flex items-center justify-center","group","transition-colors select-none","peer",{"resize-left peer/left":"left"===e,"resize-right peer/right z-10":"right"===e,"resize-top peer/top":"top"===e,"resize-bottom peer/bottom":"bottom"===e}),children:Rn("span",{className:"resize-line-wrapper",children:Rn("span",{className:"resize-line",children:Rn(On,{name:"icon-ellipsis",size:18,className:lo("text-neutral-400",("left"===e||"right"===e)&&"rotate-90")})})})})},Tu={horizontal:{width:20,height:48},vertical:{width:48,height:20}},Eu=()=>{const e=Xe(null),t=Xe(!1),n=Xe(0),r=Xe(0),o=Xe(!1),i=Ze((i=!0)=>{if(!e.current)return;const{corner:a}=yo.value;let s,l;if(So.value){const e=So.value.orientation||"horizontal",t=Tu[e];s=t.width,l=t.height}else if(t.current){const e=yo.value.lastDimensions;s=Su(e.width,0,!0),l=Su(e.height,0,!1),o.current&&(o.current=!1)}else s=n.current,l=r.current;let c=Nu(a,s,l);if(So.value){const{corner:e,orientation:t="horizontal"}=So.value,n=Tu[t],r=vo();switch(e){case"top-left":c="horizontal"===t?{x:-1,y:r.top}:{x:r.left,y:-1};break;case"bottom-left":c="horizontal"===t?{x:-1,y:window.innerHeight-n.height-r.bottom}:{x:r.left,y:window.innerHeight-n.height+1};break;case"top-right":c="horizontal"===t?{x:window.innerWidth-n.width+1,y:r.top}:{x:window.innerWidth-n.width-r.right,y:-1};break;default:c="horizontal"===t?{x:window.innerWidth-n.width+1,y:window.innerHeight-n.height-r.bottom}:{x:window.innerWidth-n.width-r.right,y:window.innerHeight-n.height+1}}}const d=i&&!(s{ko(),u.removeEventListener("transitionend",m),h&&(cancelAnimationFrame(h),h=null)};u.addEventListener("transitionend",m),p.transition="all 0.25s cubic-bezier(0, 0, 0.2, 1)",h=requestAnimationFrame(()=>{p.width=`${s}px`,p.height=`${l}px`,p.transform=`translate3d(${c.x}px, ${c.y}px, 0)`,h=null});const f=vo(),g={isFullWidth:s>=window.innerWidth-f.left-f.right,isFullHeight:l>=window.innerHeight-f.top-f.bottom,width:s,height:l,position:c};yo.value={corner:a,dimensions:g,lastDimensions:t?yo.value.lastDimensions:s>n.current?g:yo.value.lastDimensions,componentsTree:yo.value.componentsTree},d&&po(Wn,{corner:yo.value.corner,dimensions:yo.value.dimensions,lastDimensions:yo.value.lastDimensions,componentsTree:yo.value.componentsTree}),ko()},[]),a=Ze(t=>{if(t.target.closest("button, a, input, textarea, select, pre, [contenteditable], [data-react-scan-selectable]"))return;if(t.preventDefault(),!e.current)return;const n=e.current,r=n.style,{dimensions:o}=yo.value,a=t.clientX,s=t.clientY,l=o.position.x,c=o.position.y;let d=l,u=c,p=null,h=!1,m=a,f=s;const g=e=>{p||(h=!0,m=e.clientX,f=e.clientY,p=requestAnimationFrame(()=>{const e=m-a,t=f-s;d=Number(l)+e,u=Number(c)+t,r.transition="none",r.transform=`translate3d(${d}px, ${u}px, 0)`;const n=d+o.width,h=u+o.height,w=Math.max(0,-d),b=Math.max(0,n-window.innerWidth),x=Math.max(0,-u),y=Math.max(0,h-window.innerHeight),k=Math.min(o.width,w+b),_=Math.min(o.height,x+y);let N=k*o.height+_*o.width-k*_>.35*(o.width*o.height);if(!N&&Pu.options.value.showFPS){const e=d+o.width;N=e<=0||e-100>=window.innerWidth||u+o.height<=0||u>=window.innerHeight}if(N){const e=d+o.width/2,t=u+o.height/2,n=window.innerWidth/2,r=window.innerHeight/2;let a,s;a=eMath.max(x,y)?"horizontal":"vertical",yo.value={...yo.value,corner:a,lastDimensions:{...o,position:Nu(a,o.width,o.height)}};const l={corner:a,orientation:s};So.value=l,po(Un,l),po(Wn,yo.value),i(!1),document.removeEventListener("pointermove",g),document.removeEventListener("pointerup",v),p&&(cancelAnimationFrame(p),p=null)}p=null}))},v=()=>{if(!n)return;p&&(cancelAnimationFrame(p),p=null),document.removeEventListener("pointermove",g),document.removeEventListener("pointerup",v);const e=Math.abs(m-a),t=Math.abs(f-s),i=Math.sqrt(e*e+t*t);if(!h||i<60)return;const w=((e,t,n,r,o=100)=>{const i=void 0!==n?e-n:0,a=void 0!==r?t-r:0,s=window.innerWidth/2,l=window.innerHeight/2,c=i>o,d=a>o;if(c||i<-o){const e=t>l;return c?e?"bottom-right":"top-right":e?"bottom-left":"top-left"}if(d||a<-o){const t=e>s;return d?t?"bottom-right":"bottom-left":t?"top-right":"top-left"}return e>s?t>l?"bottom-right":"top-right":t>l?"bottom-left":"top-left"})(m,f,a,s,"focused"===Lu.inspectState.value.kind?80:40);if(w===yo.value.corner){r.transition="transform 0.25s cubic-bezier(0, 0, 0.2, 1)";const e=yo.value.dimensions.position;return void requestAnimationFrame(()=>{r.transform=`translate3d(${e.x}px, ${e.y}px, 0)`})}const b=Nu(w,o.width,o.height);if(d===l&&u===c)return;const x=()=>{r.transition="none",ko(),n.removeEventListener("transitionend",x),p&&(cancelAnimationFrame(p),p=null)};n.addEventListener("transitionend",x),r.transition="transform 0.25s cubic-bezier(0, 0, 0.2, 1)",requestAnimationFrame(()=>{r.transform=`translate3d(${b.x}px, ${b.y}px, 0)`}),yo.value={corner:w,dimensions:{isFullWidth:o.isFullWidth,isFullHeight:o.isFullHeight,width:o.width,height:o.height,position:b},lastDimensions:yo.value.lastDimensions,componentsTree:yo.value.componentsTree},po(Wn,{corner:w,dimensions:yo.value.dimensions,lastDimensions:yo.value.lastDimensions,componentsTree:yo.value.componentsTree})};document.addEventListener("pointermove",g),document.addEventListener("pointerup",v)},[]),s=Ze(t=>{if(t.preventDefault(),!e.current||!So.value)return;const{corner:r,orientation:o="horizontal"}=So.value,a=t.clientX,s=t.clientY;let l=!1;const c=t=>{if(l)return;const u=t.clientX-a,p=t.clientY-s;let h=!1;if("horizontal"===o?(r.endsWith("left")&&u>50||r.endsWith("right")&&u<-50)&&(h=!0):(r.startsWith("top")&&p>50||r.startsWith("bottom")&&p<-50)&&(h=!0),h){if(l=!0,So.value=null,po(Un,null),0===n.current&&e.current)requestAnimationFrame(()=>{if(e.current){e.current.style.width="min-content";const r=e.current.offsetWidth;n.current=r||300;const o=yo.value.lastDimensions,a=Su(o.width,0,!0),s=Su(o.height,0,!1);let l=t.clientX-a/2,c=t.clientY-s/2;const d=vo();l=Math.max(d.left,Math.min(l,window.innerWidth-a-d.right)),c=Math.max(d.top,Math.min(c,window.innerHeight-s-d.bottom)),yo.value={...yo.value,dimensions:{...yo.value.dimensions,position:{x:l,y:c}}},i(!0);const u=uo(Hn);_o.value=u||{view:"none"},setTimeout(()=>{if(e.current){const n=new PointerEvent("pointerdown",{clientX:t.clientX,clientY:t.clientY,pointerId:t.pointerId,bubbles:!0});e.current.dispatchEvent(n)}},100)}});else{i(!0);const e=uo(Hn);_o.value=e||{view:"none"}}document.removeEventListener("pointermove",c),document.removeEventListener("pointerup",d)}},d=()=>{document.removeEventListener("pointermove",c),document.removeEventListener("pointerup",d)};document.addEventListener("pointermove",c),document.addEventListener("pointerup",d)},[]);Je(()=>{if(!e.current)return;ho(Hn),So.value?(r.current=36,n.current=0):(e.current.style.width="min-content",r.current=36,n.current=e.current.offsetWidth);const a=vo();e.current.style.maxWidth=`calc(100vw - ${a.left+a.right}px)`,e.current.style.maxHeight=`calc(100vh - ${a.top+a.bottom}px)`,i(),"focused"===Lu.inspectState.value.kind||So.value||o.current||(yo.value={...yo.value,dimensions:{isFullWidth:!1,isFullHeight:!1,width:n.current,height:r.current,position:yo.value.dimensions.position}}),bo.value=e.current;const s=yo.subscribe(t=>{if(!e.current)return;const{x:n,y:r}=t.dimensions.position,{width:o,height:i}=t.dimensions,a=e.current;requestAnimationFrame(()=>{a.style.transform=`translate3d(${n}px, ${r}px, 0)`,a.style.width=`${o}px`,a.style.height=`${i}px`})}),l=_o.subscribe(e=>{t.current="none"!==e.view,i(),So.value||("none"!==e.view?po(Hn,e):ho(Hn))}),c=Lu.inspectState.subscribe(e=>{t.current="focused"===e.kind,i()}),d=()=>{i(!0)};return window.addEventListener("resize",d,{passive:!0}),()=>{window.removeEventListener("resize",d),l(),c(),s(),po(Wn,{...xo(),corner:yo.value.corner})}},[]);const[l,c]=Ge(!1);Je(()=>{c(!0)},[]);const d=So.value;let u="";if(d){const{orientation:e="horizontal",corner:t}=d;u="horizontal"===e?(null==t?void 0:t.endsWith("right"))?"rotate-180":"":(null==t?void 0:t.startsWith("bottom"))?"-rotate-90":"rotate-90"}return Rn(me,{children:[Rn(yu,{}),Rn(zu.Provider,{value:e.current,children:Rn("div",{id:"react-scan-toolbar",dir:"ltr",ref:e,onPointerDown:d?s:a,className:lo("fixed inset-0",d?(()=>{const{orientation:e="horizontal",corner:t}=d;return"horizontal"===e?(null==t?void 0:t.endsWith("right"))?"rounded-tl-lg rounded-bl-lg shadow-lg":"rounded-tr-lg rounded-br-lg shadow-lg":(null==t?void 0:t.startsWith("bottom"))?"rounded-tl-lg rounded-tr-lg shadow-lg":"rounded-bl-lg rounded-br-lg shadow-lg"})():"rounded-lg shadow-lg","flex flex-col","font-mono text-[13px]","user-select-none","opacity-0",d?"cursor-pointer":"cursor-move","z-[124124124124]","animate-fade-in animation-duration-300 animation-delay-300","will-change-transform","[touch-action:none]"),style:{WebkitAppRegion:"no-drag"},children:d?Rn("button",{type:"button",onClick:()=>{So.value=null,po(Un,null),0===n.current&&e.current&&requestAnimationFrame(()=>{if(e.current){e.current.style.width="min-content";const t=e.current.offsetWidth;n.current=t||300,i(!0)}});const t=uo(Hn);_o.value=t||{view:"none"}},className:"flex items-center justify-center w-full h-full text-white",title:"Expand toolbar",children:Rn(On,{name:"icon-chevron-right",size:16,className:lo("transition-transform",u)})}):Rn(me,{children:[Rn(Cu,{position:"top"}),Rn(Cu,{position:"bottom"}),Rn(Cu,{position:"left"}),Rn(Cu,{position:"right"}),Rn(gu,{})]})})})]})},zu=De(null),Au=()=>Rn("svg",{xmlns:"http://www.w3.org/2000/svg",style:"display: none;",children:[Rn("title",{children:"React Scan Icons"}),Rn("symbol",{id:"icon-inspect",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}),Rn("path",{d:"M5 3a2 2 0 0 0-2 2"}),Rn("path",{d:"M19 3a2 2 0 0 1 2 2"}),Rn("path",{d:"M5 21a2 2 0 0 1-2-2"}),Rn("path",{d:"M9 3h1"}),Rn("path",{d:"M9 21h2"}),Rn("path",{d:"M14 3h1"}),Rn("path",{d:"M3 9v1"}),Rn("path",{d:"M21 9v2"}),Rn("path",{d:"M3 14v1"})]}),Rn("symbol",{id:"icon-focus",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}),Rn("path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"})]}),Rn("symbol",{id:"icon-next",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:Rn("path",{d:"M6 9h6V5l7 7-7 7v-4H6V9z"})}),Rn("symbol",{id:"icon-previous",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:Rn("path",{d:"M18 15h-6v4l-7-7 7-7v4h6v6z"})}),Rn("symbol",{id:"icon-close",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),Rn("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]}),Rn("symbol",{id:"icon-replay",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}),Rn("path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}),Rn("path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}),Rn("path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}),Rn("circle",{cx:"12",cy:"12",r:"1"}),Rn("path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"})]}),Rn("symbol",{id:"icon-ellipsis",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("circle",{cx:"12",cy:"12",r:"1"}),Rn("circle",{cx:"19",cy:"12",r:"1"}),Rn("circle",{cx:"5",cy:"12",r:"1"})]}),Rn("symbol",{id:"icon-copy",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}),Rn("path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"})]}),Rn("symbol",{id:"icon-check",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:Rn("path",{d:"M20 6 9 17l-5-5"})}),Rn("symbol",{id:"icon-chevron-right",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:Rn("path",{d:"m9 18 6-6-6-6"})}),Rn("symbol",{id:"icon-settings",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"}),Rn("circle",{cx:"12",cy:"12",r:"3"})]}),Rn("symbol",{id:"icon-flame",viewBox:"0 0 24 24",children:Rn("path",{d:"M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"})}),Rn("symbol",{id:"icon-function",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}),Rn("path",{d:"M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"}),Rn("path",{d:"M9 11.2h5.7"})]}),Rn("symbol",{id:"icon-triangle-alert",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"}),Rn("path",{d:"M12 9v4"}),Rn("path",{d:"M12 17h.01"})]}),Rn("symbol",{id:"icon-gallery-horizontal-end",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"M2 7v10"}),Rn("path",{d:"M6 5v14"}),Rn("rect",{width:"12",height:"18",x:"10",y:"3",rx:"2"})]}),Rn("symbol",{id:"icon-search",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("circle",{cx:"11",cy:"11",r:"8"}),Rn("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"})]}),Rn("symbol",{id:"icon-lock",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}),Rn("path",{d:"M7 11V7a5 5 0 0 1 10 0v4"})]}),Rn("symbol",{id:"icon-lock-open",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}),Rn("path",{d:"M7 11V7a5 5 0 0 1 9.9-1"})]}),Rn("symbol",{id:"icon-sanil",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",children:[Rn("path",{d:"M2 13a6 6 0 1 0 12 0 4 4 0 1 0-8 0 2 2 0 0 0 4 0"}),Rn("circle",{cx:"10",cy:"13",r:"8"}),Rn("path",{d:"M2 21h12c4.4 0 8-3.6 8-8V7a2 2 0 1 0-4 0v6"}),Rn("path",{d:"M18 3 19.1 5.2"})]})]}),Mu=class extends fe{constructor(){super(...arguments),n(this,"state",{hasError:!1,error:null}),n(this,"handleReset",()=>{this.setState({hasError:!1,error:null})})}static getDerivedStateFromError(e){return{hasError:!0,error:e}}render(){var e;return this.state.hasError?Rn("div",{className:"fixed bottom-4 right-4 z-[124124124124]",children:Rn("div",{className:"p-3 bg-black rounded-lg shadow-lg w-80",children:[Rn("div",{className:"flex items-center gap-2 mb-2 text-red-400 text-sm font-medium",children:[Rn(On,{name:"icon-flame",className:"text-red-500",size:14}),"React Scan ran into a problem"]}),Rn("div",{className:"p-2 bg-black rounded font-mono text-xs text-red-300 mb-3 break-words",children:(null==(e=this.state.error)?void 0:e.message)||JSON.stringify(this.state.error)}),Rn("button",{type:"button",onClick:this.handleReset,className:"px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs font-medium transition-colors flex items-center justify-center gap-1.5",children:"Restart"})]})}):this.props.children}},$u="0.1.32",Fu=!1,Ru=["top","right","bottom","left"],Ou=e=>{if(fo(e))return{ok:!0,value:e};if(!go(e))return{ok:!1,error:`- safeArea must be a non-negative number or { top?, right?, bottom?, left? }. Got "${JSON.stringify(e)}"`};const t={};for(const n of Ru){const r=e[n];if(void 0!==r){if(!fo(r))return{ok:!1,error:`- safeArea.${n} must be a non-negative number. Got "${JSON.stringify(r)}"`};t[n]=r}}return{ok:!0,value:t}},Du=null,ju=null,Lu={wasDetailsOpen:_t(!0),isInIframe:_t(ie&&window.self!==window.top),inspectState:_t({kind:"uninitialized"}),fiberRoots:new Set,reportData:new Map,legacyReportData:new Map,lastReportTime:_t(0),interactionListeningForRenders:null,changesListeners:new Map},Pu={instrumentation:null,componentAllowList:null,options:_t({enabled:!0,log:!1,showToolbar:!0,animationSpeed:"fast",dangerouslyForceRunInProduction:!1,showFPS:!0,showNotificationCount:!0,allowInIframe:!1}),runInAllEnvironments:!1,onRender:null,Store:Lu,version:"0.5.7"};ie&&window.__REACT_SCAN_EXTENSION__&&(window.__REACT_SCAN_VERSION__=Pu.version);var Iu,Wu=e=>{const t=[],n={};for(const r in e){const o=e[r];switch(r){case"enabled":case"log":case"showToolbar":case"showNotificationCount":case"dangerouslyForceRunInProduction":case"showFPS":case"allowInIframe":case"useOffscreenCanvasWorker":"boolean"!=typeof o?t.push(`- ${r} must be a boolean. Got "${o}"`):n[r]=o;break;case"animationSpeed":["slow","fast","off"].includes(o)?n[r]=o:t.push(`- Invalid animation speed "${o}". Using default "fast"`);break;case"safeArea":{const e=Ou(o);e.ok?n.safeArea=e.value:t.push(e.error);break}case"onCommitStart":"function"!=typeof o?t.push(`- ${r} must be a function. Got "${o}"`):n.onCommitStart=o;break;case"onCommitFinish":"function"!=typeof o?t.push(`- ${r} must be a function. Got "${o}"`):n.onCommitFinish=o;break;case"onRender":"function"!=typeof o?t.push(`- ${r} must be a function. Got "${o}"`):n.onRender=o;break;default:t.push(`- Unknown option "${r}"`)}}return t.length>0&&console.warn(`[React Scan] Invalid options:\n${t.join("\n")}`),n},Uu=e=>{var t;try{const n=Wu(e);if(0===Object.keys(n).length)return;const r="showToolbar"in n&&void 0!==n.showToolbar,o={...Pu.options.value,...n},{instrumentation:i}=Pu;i&&"enabled"in n&&(i.isPaused.value=!1===n.enabled),Pu.options.value=o;try{const e=null==(t=uo("react-scan-options"))?void 0:t.enabled;"boolean"==typeof e&&(o.enabled=e)}catch(e){"verbose"===Pu.options.value._debug&&console.error("[React Scan Internal Error]","Failed to create notifications outline canvas",e)}return po("react-scan-options",(e=>{const{onCommitStart:t,onRender:n,onCommitFinish:r,...o}=e;return o})(o)),r&&Gu(!!o.showToolbar),o}catch(e){"verbose"===Pu.options.value._debug&&console.error("[React Scan Internal Error]","Failed to create notifications outline canvas",e)}},Hu=()=>Pu.options,Bu=null,Vu=()=>{if(!1===Bu)return!1;null!=Iu||(Iu=g());const e=Array.from(Iu.renderers.values());if(0===e.length)return null;for(const t of e){if("production"!==Y(t))return Bu=!1,!1}return!0},qu=()=>{try{if(!ie)return;if(!Pu.runInAllEnvironments&&Vu()&&!Pu.options.value.dangerouslyForceRunInProduction)return;(()=>{if(Fu)return;if(Fu=!0,"undefined"==typeof window)return;if(window.__REACT_GRAB__)return;if(!navigator.onLine)return;const e={referrerPolicy:"origin",keepalive:!0,priority:"low",cache:"no-store"};try{fetch(`https://www.react-grab.com/api/version?source=react-scan&v=${$u}&t=${Date.now()}`,e).then(e=>e.ok?e.text():null).then(e=>{if(!e)return;const t=e.trim();/^\d+\.\d+\.\d+/.test(t)&&t!==$u&&console.warn(`[React Scan] react-grab v${$u} is outdated (latest: v${t}). Update react-scan to pick up the newer react-grab.`)}).catch(()=>null)}catch{}})();const e=uo("react-scan-options");if(e){const t=Wu(e);Object.keys(t).length>0&&(Pu.options.value={...Pu.options.value,...t})}const t=Hu();Ya(()=>{Gu(!!t.value.showToolbar)}),ie&&setTimeout(()=>{(()=>{let e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;return!!(null==e?void 0:e._instrumentationIsActive)||c(e)||u(e)})()||console.error("[React Scan] Failed to load. Must import React Scan before React runs.")},5e3)}catch(e){"verbose"===Pu.options.value._debug&&console.error("[React Scan Internal Error]","Failed to create notifications outline canvas",e)}},Gu=e=>{var t;null==(t=window.reactScanCleanupListeners)||t.call(window);const n=ud(),r=Ju();window.reactScanCleanupListeners=()=>{n(),null==r||r()};const o=window.__REACT_SCAN_TOOLBAR_CONTAINER__;if(!e)return void(null==o||o.remove());null==o||o.remove();const{shadowRoot:i}=(()=>{if(Du&&ju)return{rootContainer:Du,shadowRoot:ju};(Du=document.createElement("div")).id="react-scan-root",ju=Du.attachShadow({mode:"open"});const e=document.createElement("style");return e.textContent='/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",\n "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";\n --color-red-300: oklch(80.8% 0.114 19.571);\n --color-red-400: oklch(70.4% 0.191 22.216);\n --color-red-500: oklch(63.7% 0.237 25.331);\n --color-red-600: oklch(57.7% 0.245 27.325);\n --color-red-950: oklch(25.8% 0.092 26.042);\n --color-yellow-300: oklch(90.5% 0.182 98.111);\n --color-yellow-500: oklch(79.5% 0.184 86.047);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-purple-400: oklch(71.4% 0.203 305.504);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-purple-800: oklch(43.8% 0.218 303.724);\n --color-gray-100: oklch(96.7% 0.003 264.542);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-zinc-200: oklch(92% 0.004 286.32);\n --color-zinc-400: oklch(70.5% 0.015 286.067);\n --color-zinc-500: oklch(55.2% 0.016 285.938);\n --color-zinc-600: oklch(44.2% 0.017 285.786);\n --color-zinc-700: oklch(37% 0.013 285.805);\n --color-zinc-800: oklch(27.4% 0.006 286.033);\n --color-zinc-900: oklch(21% 0.006 285.885);\n --color-neutral-300: oklch(87% 0 0);\n --color-neutral-400: oklch(70.8% 0 0);\n --color-neutral-500: oklch(55.6% 0 0);\n --color-neutral-700: oklch(37.1% 0 0);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 4px;\n --container-md: 448px;\n --text-xs: 12px;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 14px;\n --text-sm--line-height: calc(1.25 / 0.875);\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wide: 0.025em;\n --radius-sm: 4px;\n --radius-md: 6px;\n --radius-lg: 8px;\n --ease-in: cubic-bezier(0.4, 0, 1, 1);\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --blur-sm: 8px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n -moz-tab-size: 4;\n -o-tab-size: 4;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;\n font-feature-settings: normal;\n font-variation-settings: normal;\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::-moz-placeholder {\n opacity: 1;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::-moz-placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {\n -webkit-appearance: button;\n -moz-appearance: button;\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden="until-found"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-auto {\n pointer-events: auto;\n }\n .pointer-events-bounding-box {\n pointer-events: bounding-box;\n }\n .pointer-events-none {\n pointer-events: none;\n }\n .collapse {\n visibility: collapse;\n }\n .visible {\n visibility: visible;\n }\n .absolute {\n position: absolute;\n }\n .fixed {\n position: fixed;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .inset-x-1 {\n inset-inline: calc(var(--spacing) * 1);\n }\n .inset-y-0 {\n inset-block: calc(var(--spacing) * 0);\n }\n .start {\n inset-inline-start: var(--spacing);\n }\n .end {\n inset-inline-end: var(--spacing);\n }\n .-top-1 {\n top: calc(var(--spacing) * -1);\n }\n .-top-2\\.5 {\n top: calc(var(--spacing) * -2.5);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-0\\.5 {\n top: calc(var(--spacing) * 0.5);\n }\n .top-1\\/2 {\n top: calc(1 / 2 * 100%);\n }\n .top-2 {\n top: calc(var(--spacing) * 2);\n }\n .-right-1 {\n right: calc(var(--spacing) * -1);\n }\n .-right-2\\.5 {\n right: calc(var(--spacing) * -2.5);\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-0\\.5 {\n right: calc(var(--spacing) * 0.5);\n }\n .right-2 {\n right: calc(var(--spacing) * 2);\n }\n .right-4 {\n right: calc(var(--spacing) * 4);\n }\n .bottom-0 {\n bottom: calc(var(--spacing) * 0);\n }\n .bottom-4 {\n bottom: calc(var(--spacing) * 4);\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-3 {\n left: calc(var(--spacing) * 3);\n }\n .z-10 {\n z-index: 10;\n }\n .z-50 {\n z-index: 50;\n }\n .z-100 {\n z-index: 100;\n }\n .z-\\[214748365\\] {\n z-index: 214748365;\n }\n .z-\\[214748367\\] {\n z-index: 214748367;\n }\n .z-\\[124124124124\\] {\n z-index: 124124124124;\n }\n .container {\n width: 100%;\n @media (width >= 640px) {\n max-width: 640px;\n }\n @media (width >= 768px) {\n max-width: 768px;\n }\n @media (width >= 1024px) {\n max-width: 1024px;\n }\n @media (width >= 1280px) {\n max-width: 1280px;\n }\n @media (width >= 1536px) {\n max-width: 1536px;\n }\n }\n .m-\\[2px\\] {\n margin: 2px;\n }\n .mx-0\\.5 {\n margin-inline: calc(var(--spacing) * 0.5);\n }\n .mt-0\\.5 {\n margin-top: calc(var(--spacing) * 0.5);\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-4 {\n margin-top: calc(var(--spacing) * 4);\n }\n .mr-0\\.5 {\n margin-right: calc(var(--spacing) * 0.5);\n }\n .mr-1 {\n margin-right: calc(var(--spacing) * 1);\n }\n .mr-1\\.5 {\n margin-right: calc(var(--spacing) * 1.5);\n }\n .mr-16 {\n margin-right: calc(var(--spacing) * 16);\n }\n .mr-auto {\n margin-right: auto;\n }\n .mb-1\\.5 {\n margin-bottom: calc(var(--spacing) * 1.5);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-3 {\n margin-bottom: calc(var(--spacing) * 3);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .mb-px {\n margin-bottom: 1px;\n }\n .\\!ml-0 {\n margin-left: calc(var(--spacing) * 0) !important;\n }\n .ml-1 {\n margin-left: calc(var(--spacing) * 1);\n }\n .ml-1\\.5 {\n margin-left: calc(var(--spacing) * 1.5);\n }\n .ml-auto {\n margin-left: auto;\n }\n .block {\n display: block;\n }\n .contents {\n display: contents;\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .aspect-square {\n aspect-ratio: 1 / 1;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-4\\/5 {\n height: calc(4 / 5 * 100%);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-7 {\n height: calc(var(--spacing) * 7);\n }\n .h-8 {\n height: calc(var(--spacing) * 8);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-12 {\n height: calc(var(--spacing) * 12);\n }\n .h-\\[28px\\] {\n height: 28px;\n }\n .h-\\[48px\\] {\n height: 48px;\n }\n .h-\\[50px\\] {\n height: 50px;\n }\n .h-\\[150px\\] {\n height: 150px;\n }\n .h-\\[235px\\] {\n height: 235px;\n }\n .h-\\[calc\\(100\\%-25px\\)\\] {\n height: calc(100% - 25px);\n }\n .h-\\[calc\\(100\\%-40px\\)\\] {\n height: calc(100% - 40px);\n }\n .h-\\[calc\\(100\\%-48px\\)\\] {\n height: calc(100% - 48px);\n }\n .h-\\[calc\\(100\\%-150px\\)\\] {\n height: calc(100% - 150px);\n }\n .h-\\[calc\\(100\\%-200px\\)\\] {\n height: calc(100% - 200px);\n }\n .h-fit {\n height: -moz-fit-content;\n height: fit-content;\n }\n .h-full {\n height: 100%;\n }\n .h-screen {\n height: 100vh;\n }\n .max-h-0 {\n max-height: calc(var(--spacing) * 0);\n }\n .max-h-9 {\n max-height: calc(var(--spacing) * 9);\n }\n .max-h-40 {\n max-height: calc(var(--spacing) * 40);\n }\n .min-h-9 {\n min-height: calc(var(--spacing) * 9);\n }\n .min-h-\\[48px\\] {\n min-height: 48px;\n }\n .min-h-fit {\n min-height: -moz-fit-content;\n min-height: fit-content;\n }\n .w-1 {\n width: calc(var(--spacing) * 1);\n }\n .w-1\\/2 {\n width: calc(1 / 2 * 100%);\n }\n .w-1\\/3 {\n width: calc(1 / 3 * 100%);\n }\n .w-2\\/4 {\n width: calc(2 / 4 * 100%);\n }\n .w-3 {\n width: calc(var(--spacing) * 3);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-4\\/5 {\n width: calc(4 / 5 * 100%);\n }\n .w-6 {\n width: calc(var(--spacing) * 6);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-\\[20px\\] {\n width: 20px;\n }\n .w-\\[72px\\] {\n width: 72px;\n }\n .w-\\[90\\%\\] {\n width: 90%;\n }\n .w-\\[calc\\(100\\%-200px\\)\\] {\n width: calc(100% - 200px);\n }\n .w-fit {\n width: -moz-fit-content;\n width: fit-content;\n }\n .w-full {\n width: 100%;\n }\n .w-px {\n width: 1px;\n }\n .w-screen {\n width: 100vw;\n }\n .max-w-md {\n max-width: var(--container-md);\n }\n .min-w-0 {\n min-width: calc(var(--spacing) * 0);\n }\n .min-w-\\[200px\\] {\n min-width: 200px;\n }\n .min-w-fit {\n min-width: -moz-fit-content;\n min-width: fit-content;\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .grow {\n flex-grow: 1;\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-\\[200\\%\\] {\n --tw-translate-y: calc(200% * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .translate-y-0 {\n --tw-translate-y: calc(var(--spacing) * 0);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .scale-110 {\n --tw-scale-x: 110%;\n --tw-scale-y: 110%;\n --tw-scale-z: 110%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n .-rotate-90 {\n rotate: calc(90deg * -1);\n }\n .rotate-90 {\n rotate: 90deg;\n }\n .rotate-180 {\n rotate: 180deg;\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-fade-in {\n animation: fadeIn ease-in forwards;\n }\n .cursor-default {\n cursor: default;\n }\n .cursor-e-resize {\n cursor: e-resize;\n }\n .cursor-ew-resize {\n cursor: ew-resize;\n }\n .cursor-ew-resize {\n cursor: ew-resize;\n }\n .cursor-move {\n cursor: move;\n }\n .cursor-move {\n cursor: move;\n }\n .cursor-nesw-resize {\n cursor: nesw-resize;\n }\n .cursor-nesw-resize {\n cursor: nesw-resize;\n }\n .cursor-ns-resize {\n cursor: ns-resize;\n }\n .cursor-ns-resize {\n cursor: ns-resize;\n }\n .cursor-nwse-resize {\n cursor: nwse-resize;\n }\n .cursor-nwse-resize {\n cursor: nwse-resize;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .cursor-w-resize {\n cursor: w-resize;\n }\n .\\[touch-action\\:none\\] {\n touch-action: none;\n }\n .resize {\n resize: both;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .items-end {\n align-items: flex-end;\n }\n .items-start {\n align-items: flex-start;\n }\n .items-stretch {\n align-items: stretch;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .justify-start {\n justify-content: flex-start;\n }\n .gap-0\\.5 {\n gap: calc(var(--spacing) * 0.5);\n }\n .gap-1 {\n gap: calc(var(--spacing) * 1);\n }\n .gap-1\\.5 {\n gap: calc(var(--spacing) * 1.5);\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .space-y-1\\.5 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .gap-x-0\\.5 {\n -moz-column-gap: calc(var(--spacing) * 0.5);\n column-gap: calc(var(--spacing) * 0.5);\n }\n .gap-x-1 {\n -moz-column-gap: calc(var(--spacing) * 1);\n column-gap: calc(var(--spacing) * 1);\n }\n .gap-x-1\\.5 {\n -moz-column-gap: calc(var(--spacing) * 1.5);\n column-gap: calc(var(--spacing) * 1.5);\n }\n .gap-x-2 {\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n }\n .gap-x-3 {\n -moz-column-gap: calc(var(--spacing) * 3);\n column-gap: calc(var(--spacing) * 3);\n }\n .gap-x-4 {\n -moz-column-gap: calc(var(--spacing) * 4);\n column-gap: calc(var(--spacing) * 4);\n }\n .gap-y-0\\.5 {\n row-gap: calc(var(--spacing) * 0.5);\n }\n .gap-y-1 {\n row-gap: calc(var(--spacing) * 1);\n }\n .gap-y-2 {\n row-gap: calc(var(--spacing) * 2);\n }\n .gap-y-4 {\n row-gap: calc(var(--spacing) * 4);\n }\n .divide-y {\n :where(& > :not(:last-child)) {\n --tw-divide-y-reverse: 0;\n border-bottom-style: var(--tw-border-style);\n border-top-style: var(--tw-border-style);\n border-top-width: calc(1px * var(--tw-divide-y-reverse));\n border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));\n }\n }\n .divide-zinc-800 {\n :where(& > :not(:last-child)) {\n border-color: var(--color-zinc-800);\n }\n }\n .place-self-center {\n place-self: center;\n }\n .self-end {\n align-self: flex-end;\n }\n .truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .\\!overflow-visible {\n overflow: visible !important;\n }\n .overflow-auto {\n overflow: auto;\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-x-auto {\n overflow-x: auto;\n }\n .overflow-x-hidden {\n overflow-x: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded {\n border-radius: 4px;\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-md {\n border-radius: var(--radius-md);\n }\n .rounded-sm {\n border-radius: var(--radius-sm);\n }\n .rounded-t-lg {\n border-top-left-radius: var(--radius-lg);\n border-top-right-radius: var(--radius-lg);\n }\n .rounded-t-sm {\n border-top-left-radius: var(--radius-sm);\n border-top-right-radius: var(--radius-sm);\n }\n .rounded-l-md {\n border-top-left-radius: var(--radius-md);\n border-bottom-left-radius: var(--radius-md);\n }\n .rounded-l-sm {\n border-top-left-radius: var(--radius-sm);\n border-bottom-left-radius: var(--radius-sm);\n }\n .rounded-tl-lg {\n border-top-left-radius: var(--radius-lg);\n }\n .rounded-r-md {\n border-top-right-radius: var(--radius-md);\n border-bottom-right-radius: var(--radius-md);\n }\n .rounded-r-sm {\n border-top-right-radius: var(--radius-sm);\n border-bottom-right-radius: var(--radius-sm);\n }\n .rounded-tr-lg {\n border-top-right-radius: var(--radius-lg);\n }\n .rounded-br-lg {\n border-bottom-right-radius: var(--radius-lg);\n }\n .rounded-bl-lg {\n border-bottom-left-radius: var(--radius-lg);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-4 {\n border-style: var(--tw-border-style);\n border-width: 4px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-r {\n border-right-style: var(--tw-border-style);\n border-right-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-l-0 {\n border-left-style: var(--tw-border-style);\n border-left-width: 0px;\n }\n .border-l-1 {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-none {\n --tw-border-style: none;\n border-style: none;\n }\n .\\!border-red-500 {\n border-color: var(--color-red-500) !important;\n }\n .border-\\[\\#1e1e1e\\] {\n border-color: #1e1e1e;\n }\n .border-\\[\\#222\\] {\n border-color: #222;\n }\n .border-\\[\\#333\\] {\n border-color: #333;\n }\n .border-\\[\\#27272A\\] {\n border-color: #27272A;\n }\n .border-transparent {\n border-color: transparent;\n }\n .border-zinc-800 {\n border-color: var(--color-zinc-800);\n }\n .bg-\\[\\#0A0A0A\\] {\n background-color: #0A0A0A;\n }\n .bg-\\[\\#1D3A66\\] {\n background-color: #1D3A66;\n }\n .bg-\\[\\#1E1E1E\\] {\n background-color: #1E1E1E;\n }\n .bg-\\[\\#1a2a1a\\] {\n background-color: #1a2a1a;\n }\n .bg-\\[\\#1e1e1e\\] {\n background-color: #1e1e1e;\n }\n .bg-\\[\\#2a1515\\] {\n background-color: #2a1515;\n }\n .bg-\\[\\#4b4b4b\\] {\n background-color: #4b4b4b;\n }\n .bg-\\[\\#5f3f9a\\] {\n background-color: #5f3f9a;\n }\n .bg-\\[\\#5f3f9a\\]\\/40 {\n background-color: color-mix(in oklab, #5f3f9a 40%, transparent);\n }\n .bg-\\[\\#6a369e\\] {\n background-color: #6a369e;\n }\n .bg-\\[\\#8e61e3\\] {\n background-color: #8e61e3;\n }\n .bg-\\[\\#7521c8\\] {\n background-color: #7521c8;\n }\n .bg-\\[\\#18181B\\] {\n background-color: #18181B;\n }\n .bg-\\[\\#18181B\\]\\/50 {\n background-color: color-mix(in oklab, #18181B 50%, transparent);\n }\n .bg-\\[\\#27272A\\] {\n background-color: #27272A;\n }\n .bg-\\[\\#44444a\\] {\n background-color: #44444a;\n }\n .bg-\\[\\#141414\\] {\n background-color: #141414;\n }\n .bg-\\[\\#214379d4\\] {\n background-color: #214379d4;\n }\n .bg-\\[\\#412162\\] {\n background-color: #412162;\n }\n .bg-\\[\\#EFD81A\\] {\n background-color: #EFD81A;\n }\n .bg-\\[\\#b77116\\] {\n background-color: #b77116;\n }\n .bg-\\[\\#b94040\\] {\n background-color: #b94040;\n }\n .bg-\\[\\#d36cff\\] {\n background-color: #d36cff;\n }\n .bg-\\[\\#efd81a6b\\] {\n background-color: #efd81a6b;\n }\n .bg-black {\n background-color: var(--color-black);\n }\n .bg-black\\/40 {\n background-color: color-mix(in srgb, #000 40%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 40%, transparent);\n }\n }\n .bg-green-500\\/50 {\n background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-green-500) 50%, transparent);\n }\n }\n .bg-green-500\\/60 {\n background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-green-500) 60%, transparent);\n }\n }\n .bg-neutral-700 {\n background-color: var(--color-neutral-700);\n }\n .bg-purple-500 {\n background-color: var(--color-purple-500);\n }\n .bg-purple-500\\/90 {\n background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-purple-500) 90%, transparent);\n }\n }\n .bg-purple-800 {\n background-color: var(--color-purple-800);\n }\n .bg-red-500 {\n background-color: var(--color-red-500);\n }\n .bg-red-500\\/90 {\n background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-red-500) 90%, transparent);\n }\n }\n .bg-red-950\\/50 {\n background-color: color-mix(in srgb, oklch(25.8% 0.092 26.042) 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-red-950) 50%, transparent);\n }\n }\n .bg-transparent {\n background-color: transparent;\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .bg-yellow-300 {\n background-color: var(--color-yellow-300);\n }\n .bg-zinc-800 {\n background-color: var(--color-zinc-800);\n }\n .bg-zinc-900\\/30 {\n background-color: color-mix(in srgb, oklch(21% 0.006 285.885) 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-zinc-900) 30%, transparent);\n }\n }\n .bg-zinc-900\\/50 {\n background-color: color-mix(in srgb, oklch(21% 0.006 285.885) 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-zinc-900) 50%, transparent);\n }\n }\n .p-0 {\n padding: calc(var(--spacing) * 0);\n }\n .p-1 {\n padding: calc(var(--spacing) * 1);\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-3 {\n padding: calc(var(--spacing) * 3);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .p-5 {\n padding: calc(var(--spacing) * 5);\n }\n .p-6 {\n padding: calc(var(--spacing) * 6);\n }\n .px-1 {\n padding-inline: calc(var(--spacing) * 1);\n }\n .px-1\\.5 {\n padding-inline: calc(var(--spacing) * 1.5);\n }\n .px-2 {\n padding-inline: calc(var(--spacing) * 2);\n }\n .px-2\\.5 {\n padding-inline: calc(var(--spacing) * 2.5);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .py-0\\.5 {\n padding-block: calc(var(--spacing) * 0.5);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-1\\.5 {\n padding-block: calc(var(--spacing) * 1.5);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .py-\\[1px\\] {\n padding-block: 1px;\n }\n .py-\\[3px\\] {\n padding-block: 3px;\n }\n .py-\\[5px\\] {\n padding-block: 5px;\n }\n .pt-0 {\n padding-top: calc(var(--spacing) * 0);\n }\n .pt-2 {\n padding-top: calc(var(--spacing) * 2);\n }\n .pt-5 {\n padding-top: calc(var(--spacing) * 5);\n }\n .pr-1 {\n padding-right: calc(var(--spacing) * 1);\n }\n .pr-1\\.5 {\n padding-right: calc(var(--spacing) * 1.5);\n }\n .pr-2 {\n padding-right: calc(var(--spacing) * 2);\n }\n .pr-2\\.5 {\n padding-right: calc(var(--spacing) * 2.5);\n }\n .pb-2 {\n padding-bottom: calc(var(--spacing) * 2);\n }\n .pl-1 {\n padding-left: calc(var(--spacing) * 1);\n }\n .pl-2 {\n padding-left: calc(var(--spacing) * 2);\n }\n .pl-2\\.5 {\n padding-left: calc(var(--spacing) * 2.5);\n }\n .pl-3 {\n padding-left: calc(var(--spacing) * 3);\n }\n .pl-5 {\n padding-left: calc(var(--spacing) * 5);\n }\n .pl-6 {\n padding-left: calc(var(--spacing) * 6);\n }\n .text-left {\n text-align: left;\n }\n .font-mono {\n font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .text-\\[8px\\] {\n font-size: 8px;\n }\n .text-\\[10px\\] {\n font-size: 10px;\n }\n .text-\\[11px\\] {\n font-size: 11px;\n }\n .text-\\[13px\\] {\n font-size: 13px;\n }\n .text-\\[14px\\] {\n font-size: 14px;\n }\n .text-\\[17px\\] {\n font-size: 17px;\n }\n .leading-6 {\n --tw-leading: calc(var(--spacing) * 6);\n line-height: calc(var(--spacing) * 6);\n }\n .leading-none {\n --tw-leading: 1;\n line-height: 1;\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wide {\n --tw-tracking: var(--tracking-wide);\n letter-spacing: var(--tracking-wide);\n }\n .text-wrap {\n text-wrap: wrap;\n }\n .break-words {\n overflow-wrap: break-word;\n }\n .break-all {\n word-break: break-all;\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .whitespace-pre-wrap {\n white-space: pre-wrap;\n }\n .text-\\[\\#4ade80\\] {\n color: #4ade80;\n }\n .text-\\[\\#5a5a5a\\] {\n color: #5a5a5a;\n }\n .text-\\[\\#6E6E77\\] {\n color: #6E6E77;\n }\n .text-\\[\\#6F6F78\\] {\n color: #6F6F78;\n }\n .text-\\[\\#8E61E3\\] {\n color: #8E61E3;\n }\n .text-\\[\\#666\\] {\n color: #666;\n }\n .text-\\[\\#888\\] {\n color: #888;\n }\n .text-\\[\\#999\\] {\n color: #999;\n }\n .text-\\[\\#7346a0\\] {\n color: #7346a0;\n }\n .text-\\[\\#65656D\\] {\n color: #65656D;\n }\n .text-\\[\\#737373\\] {\n color: #737373;\n }\n .text-\\[\\#A1A1AA\\] {\n color: #A1A1AA;\n }\n .text-\\[\\#A855F7\\] {\n color: #A855F7;\n }\n .text-\\[\\#E4E4E7\\] {\n color: #E4E4E7;\n }\n .text-\\[\\#d36cff\\] {\n color: #d36cff;\n }\n .text-\\[\\#f87171\\] {\n color: #f87171;\n }\n .text-black {\n color: var(--color-black);\n }\n .text-gray-100 {\n color: var(--color-gray-100);\n }\n .text-gray-300 {\n color: var(--color-gray-300);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-green-500 {\n color: var(--color-green-500);\n }\n .text-neutral-300 {\n color: var(--color-neutral-300);\n }\n .text-neutral-400 {\n color: var(--color-neutral-400);\n }\n .text-neutral-500 {\n color: var(--color-neutral-500);\n }\n .text-purple-400 {\n color: var(--color-purple-400);\n }\n .text-red-300 {\n color: var(--color-red-300);\n }\n .text-red-400 {\n color: var(--color-red-400);\n }\n .text-red-500 {\n color: var(--color-red-500);\n }\n .text-white {\n color: var(--color-white);\n }\n .text-white\\/30 {\n color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n .text-white\\/70 {\n color: color-mix(in srgb, #fff 70%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, var(--color-white) 70%, transparent);\n }\n }\n .text-yellow-300 {\n color: var(--color-yellow-300);\n }\n .text-yellow-500 {\n color: var(--color-yellow-500);\n }\n .text-zinc-200 {\n color: var(--color-zinc-200);\n }\n .text-zinc-400 {\n color: var(--color-zinc-400);\n }\n .text-zinc-500 {\n color: var(--color-zinc-500);\n }\n .text-zinc-600 {\n color: var(--color-zinc-600);\n }\n .uppercase {\n text-transform: uppercase;\n }\n .italic {\n font-style: italic;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .opacity-50 {\n opacity: 50%;\n }\n .opacity-100 {\n opacity: 100%;\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .ring-1 {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .ring-white\\/\\[0\\.08\\] {\n --tw-ring-color: color-mix(in srgb, #fff 8%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-ring-color: color-mix(in oklab, var(--color-white) 8%, transparent);\n }\n }\n .outline {\n outline-style: var(--tw-outline-style);\n outline-width: 1px;\n }\n .filter {\n filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);\n }\n .backdrop-blur-sm {\n --tw-backdrop-blur: blur(var(--blur-sm));\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-\\[border-radius\\] {\n transition-property: border-radius;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-\\[color\\,transform\\] {\n transition-property: color,transform;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-\\[max-height\\] {\n transition-property: max-height;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-\\[opacity\\] {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-opacity {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-transform {\n transition-property: transform, translate, scale, rotate;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-none {\n transition-property: none;\n }\n .delay-0 {\n transition-delay: 0ms;\n }\n .delay-150 {\n transition-delay: 150ms;\n }\n .delay-300 {\n transition-delay: 300ms;\n }\n .\\!duration-0 {\n --tw-duration: 0ms !important;\n transition-duration: 0ms !important;\n }\n .duration-0 {\n --tw-duration: 0ms;\n transition-duration: 0ms;\n }\n .duration-120 {\n --tw-duration: 120ms;\n transition-duration: 120ms;\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .duration-300 {\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n .ease-\\[cubic-bezier\\(0\\.25\\,0\\.1\\,0\\.25\\,1\\)\\] {\n --tw-ease: cubic-bezier(0.25,0.1,0.25,1);\n transition-timing-function: cubic-bezier(0.25,0.1,0.25,1);\n }\n .ease-in {\n --tw-ease: var(--ease-in);\n transition-timing-function: var(--ease-in);\n }\n .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\n }\n .ease-out {\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n }\n .will-change-transform {\n will-change: transform;\n }\n .select-none {\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n }\n .animation-delay-0 {\n animation-delay: 0s;\n }\n .animation-delay-100 {\n animation-delay: .1s;\n }\n .animation-delay-150 {\n animation-delay: .15s;\n }\n .animation-delay-200 {\n animation-delay: .2s;\n }\n .animation-delay-300 {\n animation-delay: .3s;\n }\n .animation-delay-500 {\n animation-delay: .5s;\n }\n .animation-delay-700 {\n animation-delay: .7s;\n }\n .animation-delay-1000 {\n animation-delay: 1s;\n }\n .animation-duration-0 {\n animation-duration: 0s;\n }\n .animation-duration-100 {\n animation-duration: .1s;\n }\n .animation-duration-200 {\n animation-duration: .2s;\n }\n .animation-duration-300 {\n animation-duration: .3s;\n }\n .animation-duration-500 {\n animation-duration: .5s;\n }\n .animation-duration-700 {\n animation-duration: .7s;\n }\n .animation-duration-1000 {\n animation-duration: 1s;\n }\n .group-hover\\:bg-\\[\\#5b2d89\\] {\n &:is(:where(.group):hover *) {\n @media (hover: hover) {\n background-color: #5b2d89;\n }\n }\n }\n .group-hover\\:bg-\\[\\#6a6a6a\\] {\n &:is(:where(.group):hover *) {\n @media (hover: hover) {\n background-color: #6a6a6a;\n }\n }\n }\n .group-hover\\:bg-\\[\\#21437982\\] {\n &:is(:where(.group):hover *) {\n @media (hover: hover) {\n background-color: #21437982;\n }\n }\n }\n .group-hover\\:bg-\\[\\#efda1a2f\\] {\n &:is(:where(.group):hover *) {\n @media (hover: hover) {\n background-color: #efda1a2f;\n }\n }\n }\n .group-hover\\:opacity-100 {\n &:is(:where(.group):hover *) {\n @media (hover: hover) {\n opacity: 100%;\n }\n }\n }\n .peer-hover\\/bottom\\:rounded-b-none {\n &:is(:where(.peer\\/bottom):hover ~ *) {\n @media (hover: hover) {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n }\n .peer-hover\\/left\\:rounded-l-none {\n &:is(:where(.peer\\/left):hover ~ *) {\n @media (hover: hover) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n }\n }\n .peer-hover\\/right\\:rounded-r-none {\n &:is(:where(.peer\\/right):hover ~ *) {\n @media (hover: hover) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n }\n }\n .peer-hover\\/top\\:rounded-t-none {\n &:is(:where(.peer\\/top):hover ~ *) {\n @media (hover: hover) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n }\n }\n .after\\:absolute {\n &::after {\n content: var(--tw-content);\n position: absolute;\n }\n }\n .after\\:inset-0 {\n &::after {\n content: var(--tw-content);\n inset: calc(var(--spacing) * 0);\n }\n }\n .after\\:top-\\[100\\%\\] {\n &::after {\n content: var(--tw-content);\n top: 100%;\n }\n }\n .after\\:left-1\\/2 {\n &::after {\n content: var(--tw-content);\n left: calc(1 / 2 * 100%);\n }\n }\n .after\\:h-\\[6px\\] {\n &::after {\n content: var(--tw-content);\n height: 6px;\n }\n }\n .after\\:w-\\[10px\\] {\n &::after {\n content: var(--tw-content);\n width: 10px;\n }\n }\n .after\\:-translate-x-1\\/2 {\n &::after {\n content: var(--tw-content);\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n }\n .after\\:animate-\\[fadeOut_1s_ease-out_forwards\\] {\n &::after {\n content: var(--tw-content);\n animation: fadeOut 1s ease-out forwards;\n }\n }\n .after\\:border-t-\\[6px\\] {\n &::after {\n content: var(--tw-content);\n border-top-style: var(--tw-border-style);\n border-top-width: 6px;\n }\n }\n .after\\:border-r-\\[5px\\] {\n &::after {\n content: var(--tw-content);\n border-right-style: var(--tw-border-style);\n border-right-width: 5px;\n }\n }\n .after\\:border-l-\\[5px\\] {\n &::after {\n content: var(--tw-content);\n border-left-style: var(--tw-border-style);\n border-left-width: 5px;\n }\n }\n .after\\:border-t-white {\n &::after {\n content: var(--tw-content);\n border-top-color: var(--color-white);\n }\n }\n .after\\:border-r-transparent {\n &::after {\n content: var(--tw-content);\n border-right-color: transparent;\n }\n }\n .after\\:border-l-transparent {\n &::after {\n content: var(--tw-content);\n border-left-color: transparent;\n }\n }\n .after\\:bg-purple-500\\/30 {\n &::after {\n content: var(--tw-content);\n background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-purple-500) 30%, transparent);\n }\n }\n }\n .after\\:content-\\[\\"\\"\\] {\n &::after {\n --tw-content: "";\n content: var(--tw-content);\n }\n }\n .focus-within\\:border-\\[\\#454545\\] {\n &:focus-within {\n border-color: #454545;\n }\n }\n .hover\\:bg-\\[\\#0f0f0f\\] {\n &:hover {\n @media (hover: hover) {\n background-color: #0f0f0f;\n }\n }\n }\n .hover\\:bg-\\[\\#5f3f9a\\]\\/20 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in oklab, #5f3f9a 20%, transparent);\n }\n }\n }\n .hover\\:bg-\\[\\#5f3f9a\\]\\/40 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in oklab, #5f3f9a 40%, transparent);\n }\n }\n }\n .hover\\:bg-\\[\\#18181B\\] {\n &:hover {\n @media (hover: hover) {\n background-color: #18181B;\n }\n }\n }\n .hover\\:bg-\\[\\#34343b\\] {\n &:hover {\n @media (hover: hover) {\n background-color: #34343b;\n }\n }\n }\n .hover\\:bg-red-600 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-red-600);\n }\n }\n }\n .hover\\:bg-zinc-700 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-zinc-700);\n }\n }\n }\n .hover\\:bg-zinc-800\\/50 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, oklch(27.4% 0.006 286.033) 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-zinc-800) 50%, transparent);\n }\n }\n }\n }\n .hover\\:text-neutral-300 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-neutral-300);\n }\n }\n }\n .hover\\:text-white {\n &:hover {\n @media (hover: hover) {\n color: var(--color-white);\n }\n }\n }\n}\n* {\n outline: none !important;\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n &::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n }\n &::-webkit-scrollbar-track {\n border-radius: 10px;\n background: transparent;\n }\n &::-webkit-scrollbar-thumb {\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.3);\n }\n &::-webkit-scrollbar-thumb:hover {\n background: rgba(255, 255, 255, 0.4);\n }\n &::-webkit-scrollbar-corner {\n background: transparent;\n }\n}\n@-moz-document url-prefix() {\n * {\n scrollbar-width: thin;\n scrollbar-color: rgba(255, 255, 255, 0.4) transparent;\n scrollbar-width: 6px;\n }\n}\nbutton {\n &:hover {\n @media (hover: hover) {\n background-image: none;\n }\n }\n --tw-outline-style: none;\n outline-style: none;\n --tw-border-style: none;\n border-style: none;\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n cursor: pointer;\n}\ninput {\n --tw-outline-style: none;\n outline-style: none;\n --tw-border-style: none;\n border-style: none;\n background-color: transparent;\n background-image: none;\n &::-moz-placeholder {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n &::placeholder {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n &::-moz-placeholder {\n color: var(--color-neutral-500);\n }\n &::placeholder {\n color: var(--color-neutral-500);\n }\n &::-moz-placeholder {\n font-style: italic;\n }\n &::placeholder {\n font-style: italic;\n }\n &:-moz-placeholder {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n &:placeholder-shown {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n}\nsvg {\n height: auto;\n width: auto;\n pointer-events: none;\n}\n.with-data-text {\n overflow: hidden;\n &::before {\n content: attr(data-text);\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n}\n#react-scan-toolbar {\n position: fixed;\n top: calc(var(--spacing) * 0);\n left: calc(var(--spacing) * 0);\n display: flex;\n flex-direction: column;\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;\n font-size: 13px;\n color: var(--color-white);\n background-color: var(--color-black);\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n cursor: move;\n opacity: 0%;\n z-index: 2147483678;\n animation: fadeIn ease-in forwards;\n animation-delay: .3s;\n animation-duration: .3s;\n --tw-shadow: 0 4px 12px var(--tw-shadow-color, rgba(0,0,0,0.2));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n place-self: start;\n will-change: transform;\n backface-visibility: hidden;\n}\n#react-scan-toolbar pre,\n#react-scan-toolbar textarea,\n#react-scan-toolbar input[type=\'text\'],\n#react-scan-toolbar input[type=\'search\'],\n#react-scan-toolbar [data-react-scan-selectable] {\n -webkit-user-select: text;\n -moz-user-select: text;\n user-select: text;\n cursor: text;\n}\n.button {\n &:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n &:active {\n background: rgba(255, 255, 255, 0.15);\n }\n}\n.resize-line-wrapper {\n position: absolute;\n overflow: hidden;\n}\n.resize-line {\n position: absolute;\n inset: calc(var(--spacing) * 0);\n overflow: hidden;\n background-color: var(--color-black);\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n svg {\n position: absolute;\n top: calc(1 / 2 * 100%);\n left: calc(1 / 2 * 100%);\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.resize-right,\n.resize-left {\n inset-block: calc(var(--spacing) * 0);\n width: calc(var(--spacing) * 6);\n cursor: ew-resize;\n .resize-line-wrapper {\n inset-block: calc(var(--spacing) * 0);\n width: calc(1 / 2 * 100%);\n }\n &:hover {\n .resize-line {\n --tw-translate-x: calc(var(--spacing) * 0);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n }\n}\n.resize-right {\n right: calc(var(--spacing) * 0);\n --tw-translate-x: calc(1 / 2 * 100%);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n .resize-line-wrapper {\n right: calc(var(--spacing) * 0);\n }\n .resize-line {\n border-top-right-radius: var(--radius-lg);\n border-bottom-right-radius: var(--radius-lg);\n --tw-translate-x: -100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.resize-left {\n left: calc(var(--spacing) * 0);\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n .resize-line-wrapper {\n left: calc(var(--spacing) * 0);\n }\n .resize-line {\n border-top-left-radius: var(--radius-lg);\n border-bottom-left-radius: var(--radius-lg);\n --tw-translate-x: 100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.resize-top,\n.resize-bottom {\n inset-inline: calc(var(--spacing) * 0);\n height: calc(var(--spacing) * 6);\n cursor: ns-resize;\n .resize-line-wrapper {\n inset-inline: calc(var(--spacing) * 0);\n height: calc(1 / 2 * 100%);\n }\n &:hover {\n .resize-line {\n --tw-translate-y: calc(var(--spacing) * 0);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n }\n}\n.resize-top {\n top: calc(var(--spacing) * 0);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n .resize-line-wrapper {\n top: calc(var(--spacing) * 0);\n }\n .resize-line {\n border-top-left-radius: var(--radius-lg);\n border-top-right-radius: var(--radius-lg);\n --tw-translate-y: 100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.resize-bottom {\n bottom: calc(var(--spacing) * 0);\n --tw-translate-y: calc(1 / 2 * 100%);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n .resize-line-wrapper {\n bottom: calc(var(--spacing) * 0);\n }\n .resize-line {\n border-bottom-right-radius: var(--radius-lg);\n border-bottom-left-radius: var(--radius-lg);\n --tw-translate-y: -100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.react-scan-header {\n display: flex;\n align-items: center;\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n padding-right: calc(var(--spacing) * 2);\n padding-left: calc(var(--spacing) * 3);\n min-height: calc(var(--spacing) * 9);\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n border-color: #222;\n overflow: hidden;\n white-space: nowrap;\n}\n.react-scan-replay-button,\n.react-scan-close-button {\n display: flex;\n align-items: center;\n padding: calc(var(--spacing) * 1);\n min-width: -moz-fit-content;\n min-width: fit-content;\n border-radius: 4px;\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 300ms;\n transition-duration: 300ms;\n}\n.react-scan-replay-button {\n position: relative;\n overflow: hidden;\n background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 50%, transparent) !important;\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-purple-500) 50%, transparent) !important;\n }\n &:hover {\n background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 25%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-purple-500) 25%, transparent);\n }\n }\n &.disabled {\n opacity: 50%;\n pointer-events: none;\n }\n &:before {\n content: "";\n position: absolute;\n inset: calc(var(--spacing) * 0);\n --tw-translate-x: -100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n animation: shimmer 2s infinite;\n background: linear-gradient(\n to right,\n transparent,\n rgba(142, 97, 227, 0.3),\n transparent\n );\n }\n}\n.react-scan-close-button {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n &:hover {\n background-color: color-mix(in srgb, #fff 15%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 15%, transparent);\n }\n }\n}\n@keyframes shimmer {\n 100% {\n transform: translateX(100%);\n }\n}\n.react-section-header {\n position: sticky;\n z-index: 100;\n display: flex;\n align-items: center;\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n padding-inline: calc(var(--spacing) * 3);\n height: calc(var(--spacing) * 7);\n width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: #888;\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n border-color: #222;\n background-color: #0a0a0a;\n}\n.react-scan-section {\n display: flex;\n flex-direction: column;\n padding-inline: calc(var(--spacing) * 2);\n color: #888;\n &::before {\n content: var(--tw-content);\n color: var(--color-gray-500);\n }\n &::before {\n --tw-content: attr(data-section);\n content: var(--tw-content);\n }\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n > .react-scan-property {\n margin-left: calc(14px * -1);\n }\n}\n.react-scan-property {\n position: relative;\n display: flex;\n flex-direction: column;\n padding-left: calc(var(--spacing) * 8);\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n border-color: transparent;\n overflow: hidden;\n}\n.react-scan-property-content {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-height: calc(var(--spacing) * 7);\n max-width: 100%;\n overflow: hidden;\n}\n.react-scan-string {\n color: #9ecbff;\n}\n.react-scan-number {\n color: #79c7ff;\n}\n.react-scan-boolean {\n color: #56b6c2;\n}\n.react-scan-key {\n width: -moz-fit-content;\n width: fit-content;\n max-width: calc(var(--spacing) * 60);\n white-space: nowrap;\n color: var(--color-white);\n}\n.react-scan-input {\n color: var(--color-white);\n background-color: var(--color-black);\n}\n@keyframes blink {\n from {\n opacity: 1;\n }\n to {\n opacity: 0;\n }\n}\n.react-scan-arrow {\n position: absolute;\n top: calc(var(--spacing) * 0);\n left: calc(var(--spacing) * 7);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n height: calc(var(--spacing) * 7);\n width: calc(var(--spacing) * 6);\n --tw-translate-x: -100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n z-index: 10;\n > svg {\n transition-property: transform, translate, scale, rotate;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n}\n.react-scan-nested {\n position: relative;\n overflow: hidden;\n &:before {\n content: "";\n position: absolute;\n top: calc(var(--spacing) * 0);\n left: calc(var(--spacing) * 0);\n height: 100%;\n width: 1px;\n background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-gray-500) 30%, transparent);\n }\n }\n}\n.react-scan-settings {\n position: absolute;\n inset: calc(var(--spacing) * 0);\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing) * 4);\n padding-inline: calc(var(--spacing) * 4);\n padding-block: calc(var(--spacing) * 2);\n color: #888;\n > div {\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n}\n.react-scan-preview-line {\n position: relative;\n display: flex;\n min-height: calc(var(--spacing) * 7);\n align-items: center;\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n}\n.react-scan-flash-overlay {\n position: absolute;\n inset: calc(var(--spacing) * 0);\n opacity: 0%;\n z-index: 50;\n pointer-events: none;\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n mix-blend-mode: multiply;\n background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-purple-500) 90%, transparent);\n }\n}\n.react-scan-toggle {\n position: relative;\n display: inline-flex;\n height: calc(var(--spacing) * 6);\n width: calc(var(--spacing) * 10);\n input {\n position: absolute;\n inset: calc(var(--spacing) * 0);\n z-index: 20;\n opacity: 0%;\n cursor: pointer;\n height: 100%;\n width: 100%;\n }\n input:checked {\n + div {\n background-color: #5f3f9a;\n &::before {\n --tw-translate-x: 100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n left: auto;\n border-color: #5f3f9a;\n }\n }\n }\n > div {\n position: absolute;\n inset: calc(var(--spacing) * 1);\n background-color: var(--color-neutral-700);\n border-radius: calc(infinity * 1px);\n pointer-events: none;\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 300ms;\n transition-duration: 300ms;\n &:before {\n --tw-content: \'\';\n content: var(--tw-content);\n position: absolute;\n top: calc(1 / 2 * 100%);\n left: calc(var(--spacing) * 0);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n height: calc(var(--spacing) * 4);\n width: calc(var(--spacing) * 4);\n background-color: var(--color-white);\n border-style: var(--tw-border-style);\n border-width: 2px;\n border-color: var(--color-neutral-700);\n border-radius: calc(infinity * 1px);\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n }\n}\n.react-scan-flash-active {\n opacity: 40%;\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 300ms;\n transition-duration: 300ms;\n}\n.react-scan-inspector-overlay {\n display: flex;\n flex-direction: column;\n opacity: 0%;\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 200ms;\n transition-duration: 200ms;\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n will-change: opacity;\n &.fade-out {\n opacity: 0%;\n }\n &.fade-in {\n opacity: 100%;\n }\n}\n.react-scan-what-changed {\n ul {\n list-style-type: disc;\n padding-left: calc(var(--spacing) * 4);\n }\n li {\n white-space: nowrap;\n > div {\n display: flex;\n align-items: center;\n justify-content: space-between;\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n }\n }\n}\n.count-badge {\n display: flex;\n align-items: center;\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n padding-inline: calc(var(--spacing) * 1.5);\n padding-block: calc(var(--spacing) * 0.5);\n border-radius: 4px;\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n color: #a855f7;\n --tw-numeric-spacing: tabular-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n background-color: color-mix(in oklab, #a855f7 10%, transparent);\n transform-origin: center;\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n transition-delay: 150ms;\n --tw-duration: 300ms;\n transition-duration: 300ms;\n}\n.count-flash {\n animation: countFlash .3s ease-out forwards;\n}\n.count-flash-white {\n animation: countFlashShake .3s ease-out forwards;\n transition-delay: 500ms !important;\n}\n.change-scope {\n display: flex;\n align-items: center;\n -moz-column-gap: calc(var(--spacing) * 1);\n column-gap: calc(var(--spacing) * 1);\n color: #666;\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;\n > div {\n padding-inline: calc(var(--spacing) * 1.5);\n padding-block: calc(var(--spacing) * 0.5);\n border-radius: 4px;\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n --tw-numeric-spacing: tabular-nums;\n font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);\n transform-origin: center;\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n transition-delay: 150ms;\n --tw-duration: 300ms;\n transition-duration: 300ms;\n &[data-flash="true"] {\n background-color: color-mix(in oklab, #a855f7 10%, transparent);\n color: #a855f7;\n }\n }\n}\n.react-scan-slider {\n position: relative;\n min-height: calc(var(--spacing) * 6);\n > input {\n position: absolute;\n inset: calc(var(--spacing) * 0);\n opacity: 0%;\n }\n &:before {\n --tw-content: \'\';\n content: var(--tw-content);\n position: absolute;\n inset-inline: calc(var(--spacing) * 0);\n top: calc(1 / 2 * 100%);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n height: calc(var(--spacing) * 1.5);\n background-color: color-mix(in oklab, #8e61e3 40%, transparent);\n border-radius: var(--radius-lg);\n pointer-events: none;\n }\n &:after {\n --tw-content: \'\';\n content: var(--tw-content);\n position: absolute;\n inset-inline: calc(var(--spacing) * 0);\n inset-block: calc(var(--spacing) * -2);\n z-index: calc(10 * -1);\n }\n span {\n position: absolute;\n top: calc(1 / 2 * 100%);\n left: calc(var(--spacing) * 0);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n height: calc(var(--spacing) * 2.5);\n width: calc(var(--spacing) * 2.5);\n border-radius: var(--radius-lg);\n background-color: #8e61e3;\n pointer-events: none;\n transition-property: transform, translate, scale, rotate;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 75ms;\n transition-duration: 75ms;\n }\n}\n.resize-v-line {\n display: flex;\n align-items: center;\n justify-content: center;\n max-width: calc(var(--spacing) * 1);\n min-width: calc(var(--spacing) * 1);\n height: 100%;\n width: 100%;\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n &:hover,\n &:active {\n > span {\n background-color: #222;\n }\n svg {\n opacity: 100%;\n }\n }\n &::before {\n --tw-content: "";\n content: var(--tw-content);\n position: absolute;\n inset: calc(var(--spacing) * 0);\n left: calc(1 / 2 * 100%);\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n width: 1px;\n background-color: #222;\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n > span {\n position: absolute;\n top: calc(1 / 2 * 100%);\n left: calc(1 / 2 * 100%);\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n height: 18px;\n width: calc(var(--spacing) * 1.5);\n border-radius: 4px;\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n svg {\n position: absolute;\n top: calc(1 / 2 * 100%);\n left: calc(1 / 2 * 100%);\n --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n rotate: 90deg;\n color: var(--color-neutral-400);\n opacity: 0%;\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n z-index: 50;\n }\n}\n.tree-node-search-highlight {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n span {\n padding-block: 1px;\n border-radius: var(--radius-sm);\n background-color: var(--color-yellow-300);\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n color: var(--color-black);\n }\n .single {\n margin-right: 1px;\n padding-inline: 2px;\n }\n .regex {\n padding-inline: 2px;\n }\n .start {\n margin-left: 1px;\n border-top-left-radius: var(--radius-sm);\n border-bottom-left-radius: var(--radius-sm);\n }\n .end {\n margin-right: 1px;\n border-top-right-radius: var(--radius-sm);\n border-bottom-right-radius: var(--radius-sm);\n }\n .middle {\n margin-inline: 1px;\n border-radius: var(--radius-sm);\n }\n}\n.react-scan-toolbar-notification {\n position: absolute;\n inset-inline: calc(var(--spacing) * 0);\n display: flex;\n align-items: center;\n -moz-column-gap: calc(var(--spacing) * 2);\n column-gap: calc(var(--spacing) * 2);\n padding: calc(var(--spacing) * 1);\n padding-left: calc(var(--spacing) * 2);\n font-size: 10px;\n color: var(--color-neutral-300);\n background-color: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n transition-property: transform, translate, scale, rotate;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n &:before {\n --tw-content: \'\';\n content: var(--tw-content);\n position: absolute;\n inset-inline: calc(var(--spacing) * 0);\n background-color: var(--color-black);\n height: calc(var(--spacing) * 2);\n }\n &.position-top {\n top: 100%;\n --tw-translate-y: -100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n border-bottom-right-radius: var(--radius-lg);\n border-bottom-left-radius: var(--radius-lg);\n &::before {\n top: calc(var(--spacing) * 0);\n --tw-translate-y: -100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n }\n &.position-bottom {\n bottom: 100%;\n --tw-translate-y: 100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n border-top-left-radius: var(--radius-lg);\n border-top-right-radius: var(--radius-lg);\n &::before {\n bottom: calc(var(--spacing) * 0);\n --tw-translate-y: 100%;\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n }\n &.is-open {\n --tw-translate-y: calc(var(--spacing) * 0);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.react-scan-header-item {\n position: absolute;\n inset: calc(var(--spacing) * 0);\n --tw-translate-y: calc(200% * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n transition-property: transform, translate, scale, rotate;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 300ms;\n transition-duration: 300ms;\n &.is-visible {\n --tw-translate-y: calc(var(--spacing) * 0);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n}\n.react-scan-components-tree:has(.resize-v-line:hover, .resize-v-line:active)\n .tree {\n overflow: hidden;\n}\n.react-scan-expandable {\n display: grid;\n grid-template-rows: 0fr;\n overflow: hidden;\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 75ms;\n transition-duration: 75ms;\n transition-timing-function: ease-out;\n > * {\n min-height: 0;\n }\n &.react-scan-expanded {\n grid-template-rows: 1fr;\n transition-duration: 100ms;\n }\n}\n@property --tw-translate-x {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-scale-x {\n syntax: "*";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: "*";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: "*";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-rotate-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: "*";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-divide-y-reverse {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-outline-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-drop-shadow-alpha {\n syntax: "";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-drop-shadow-size {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-blur {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: "*";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: "*";\n inherits: false;\n}\n@property --tw-duration {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ease {\n syntax: "*";\n inherits: false;\n}\n@property --tw-content {\n syntax: "*";\n initial-value: "";\n inherits: false;\n}\n@property --tw-ordinal {\n syntax: "*";\n inherits: false;\n}\n@property --tw-slashed-zero {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-figure {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-spacing {\n syntax: "*";\n inherits: false;\n}\n@property --tw-numeric-fraction {\n syntax: "*";\n inherits: false;\n}\n@keyframes fadeIn {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n@keyframes fadeOut {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n}\n@keyframes countFlash {\n 0% {\n background-color: rgba(168, 85, 247, 0.3);\n transform: scale(1.05);\n }\n 100% {\n background-color: rgba(168, 85, 247, 0.1);\n transform: scale(1);\n }\n}\n@keyframes countFlashShake {\n 0% {\n transform: translateX(0);\n }\n 25% {\n transform: translateX(-5px);\n }\n 50% {\n transform: translateX(5px) scale(1.1);\n }\n 75% {\n transform: translateX(-5px);\n }\n 100% {\n transform: translateX(0);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-divide-y-reverse: 0;\n --tw-border-style: solid;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-outline-style: solid;\n --tw-blur: initial;\n --tw-brightness: initial;\n --tw-contrast: initial;\n --tw-grayscale: initial;\n --tw-hue-rotate: initial;\n --tw-invert: initial;\n --tw-opacity: initial;\n --tw-saturate: initial;\n --tw-sepia: initial;\n --tw-drop-shadow: initial;\n --tw-drop-shadow-color: initial;\n --tw-drop-shadow-alpha: 100%;\n --tw-drop-shadow-size: initial;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n --tw-duration: initial;\n --tw-ease: initial;\n --tw-content: "";\n --tw-ordinal: initial;\n --tw-slashed-zero: initial;\n --tw-numeric-figure: initial;\n --tw-numeric-spacing: initial;\n --tw-numeric-fraction: initial;\n }\n }\n}\n',ju.appendChild(e),document.documentElement.appendChild(Du),{rootContainer:Du,shadowRoot:ju}})();(e=>{const t=document.createElement("div");t.id="react-scan-toolbar-root",window.__REACT_SCAN_TOOLBAR_CONTAINER__=t,e.appendChild(t),Oe(Rn(Mu,{children:Rn(me,{children:[Rn(Au,{}),Rn(Eu,{})]})}),t);const n=t.remove.bind(t);t.remove=()=>{window.__REACT_SCAN_TOOLBAR_CONTAINER__=void 0,t.hasChildNodes()&&(Oe(null,t),Oe(null,t)),n()}})(i)},Ju=()=>{try{return(e=>{if(Ld=document.createElement("canvas"),!(Pd=Ld.getContext("2d",{alpha:!0})))return null;const t=window.devicePixelRatio||1,{innerWidth:n,innerHeight:r}=window;Ld.style.width=`${n}px`,Ld.style.height=`${r}px`,Ld.width=n*t,Ld.height=r*t,Ld.style.position="fixed",Ld.style.left="0",Ld.style.top="0",Ld.style.pointerEvents="none",Ld.style.zIndex="2147483600",Pd.scale(t,t),e.appendChild(Ld),Vd&&window.removeEventListener("resize",Vd);const o=()=>{if(!Ld||!Pd)return;const e=window.devicePixelRatio||1,{innerWidth:t,innerHeight:n}=window;Ld.style.width=`${t}px`,Ld.style.height=`${n}px`,Ld.width=t*e,Ld.height=n*e,Pd.scale(e,e),Bd()};return Vd=o,window.addEventListener("resize",o),Id.subscribe(()=>{requestAnimationFrame(()=>{Bd()})}),qd})(document.documentElement)}catch(e){"verbose"===Pu.options.value._debug&&console.error("[React Scan Internal Error]","Failed to create notifications outline canvas",e)}},Yu=(e={})=>{Uu(e);(!Lu.isInIframe.value||Pu.options.value.allowInIframe||Pu.runInAllEnvironments)&&(!1===e.enabled&&!0!==e.showToolbar||qu())},Xu=new WeakSet;ie&&(Yu(),window.reactScan=Yu) -/*! Bundled license information: +/** + * Copyright 2025 Aiden Bai, Million Software, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +(function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n=Object.defineProperty,r=(e,t)=>()=>(e&&(t=e(e=0)),t),i=(e,t)=>{let r={};for(var i in e)n(r,i,{get:e[i],enumerable:!0});return t||n(r,Symbol.toStringTag,{value:`Module`}),r};Array.prototype.toSorted||Object.defineProperty(Array.prototype,`toSorted`,{value:function(e){return[...this].sort(e)},writable:!0,configurable:!0});var a,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,ee,te,ne,x=r((()=>{a=e=>Object.assign(e,{[Symbol.dispose]:e}),o=`0.6.0`,s=`bippy-${o}`,c=Object.defineProperty,l=Object.prototype.hasOwnProperty,u=()=>{},d=e=>{try{Function.prototype.toString.call(e).indexOf(`^_^`)>-1&&setTimeout(()=>{throw Error(`React is running in production mode, but dead code elimination has not been applied. Read how to correctly configure React for production: https://reactjs.org/link/perf-use-production-build`)})}catch{}},f=(e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__)=>!!(e&&`getFiberRoots`in e),p=!1,h=(e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__)=>p?!0:(e&&typeof e.inject==`function`&&(m=e.inject.toString()),!!(m!=null&&m.includes(`(injected)`))),g=new Set,_=new Set,v=e=>{e&&g.add(e);let t=new Map,n=0,r={_instrumentationIsActive:!1,_instrumentationSource:s,checkDCE:d,hasUnsupportedRendererAttached:!1,inject(e){let i=++n;return t.set(i,e),_.add(e),r._instrumentationIsActive||(r._instrumentationIsActive=!0,g.forEach(e=>e())),i},on:u,onCommitFiberRoot:u,onCommitFiberUnmount:u,onPostCommitFiberRoot:u,renderers:t,supportsFiber:!0,supportsFlight:!0};try{c(globalThis,`__REACT_DEVTOOLS_GLOBAL_HOOK__`,{configurable:!0,enumerable:!0,get(){return r},set(t){if(t&&typeof t==`object`){let n=r.renderers;r=t,n.size>0&&(n.forEach((e,n)=>{_.add(e),t.renderers.set(n,e)}),y(e))}}});let t=window.hasOwnProperty,n=!1;c(window,`hasOwnProperty`,{configurable:!0,value:function(...e){try{if(!n&&e[0]===`__REACT_DEVTOOLS_GLOBAL_HOOK__`)return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__=void 0,n=!0,-0}catch{}return t.apply(this,e)},writable:!0})}catch{y(e)}return r},y=e=>{e&&g.add(e);try{let t=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!t)return;if(!t._instrumentationSource){t.checkDCE=d,t.supportsFiber=!0,t.supportsFlight=!0,t.hasUnsupportedRendererAttached=!1,t._instrumentationSource=s,t._instrumentationIsActive=!1;let e=f(t);if(e||(t.on=u),t.renderers.size){t._instrumentationIsActive=!0,g.forEach(e=>e());return}let n=t.inject,r=h(t);r&&!e&&(p=!0,t.inject({scheduleRefresh(){}})&&(t._instrumentationIsActive=!0)),t.inject=e=>{let i=n(e);return _.add(e),r&&t.renderers.set(i,e),t._instrumentationIsActive=!0,g.forEach(e=>e()),i}}(t.renderers.size||t._instrumentationIsActive||h())&&(e==null||e())}catch{}},b=()=>l.call(globalThis,`__REACT_DEVTOOLS_GLOBAL_HOOK__`),ee=e=>b()?(y(e),globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__):v(e),te=()=>{var e,t;return!!(typeof window<`u`&&((e=window.document)!=null&&e.createElement||((t=window.navigator)==null?void 0:t.product)===`ReactNative`))},ne=()=>{try{te()&&ee()}catch{}}})),S=r((()=>{x(),ne()}));function re(e,t,n=!1){if(!e)return null;let r=t(e);if(r instanceof Promise)return(async()=>{if(await r===!0)return e;let i=n?e.return:e.child;for(;i;){let e=await ge(i,t,n);if(e)return e;i=n?null:i.sibling}return null})();if(r===!0)return e;let i=n?e.return:e.child;for(;i;){let e=he(i,t,n);if(e)return e;i=n?null:i.sibling}return null}var ie,ae,oe,se,ce,le,ue,de,fe,C,pe,me,he,ge,w,_e,ve,ye,be,xe,Se,Ce,we,Te,Ee,De,Oe,ke,Ae,je,Me,Ne,Pe,Fe,Ie,Le,Re,ze,Be,Ve,He,Ue,We=r((()=>{x(),ie=60111,ae=`Symbol(react.concurrent_mode)`,oe=`Symbol(react.async_mode)`,se=13366,ce=e=>{switch(e.tag){case 5:case 26:case 27:return!0;default:return typeof e.type==`string`}},le=e=>{switch(e.tag){case 1:case 11:case 0:case 14:case 15:return!0;default:return!1}},ue=(e,t)=>{try{var n;let r=e.dependencies,i=(n=e.alternate)==null?void 0:n.dependencies;if(!r||!i||typeof r!=`object`||!(`firstContext`in r)||typeof i!=`object`||!(`firstContext`in i))return!1;let a=r.firstContext,o=i.firstContext;for(;a&&typeof a==`object`&&`memoizedValue`in a||o&&typeof o==`object`&&`memoizedValue`in o;){if(t(a,o)===!0)return!0;a=a==null?void 0:a.next,o=o==null?void 0:o.next}}catch{}return!1},de=e=>{var t,n,r;let i=e.memoizedProps,a=((t=e.alternate)==null?void 0:t.memoizedProps)||{},o=(n=(r=e.flags)==null?e.effectTag:r)==null?0:n;switch(e.tag){case 1:case 9:case 11:case 0:case 14:case 15:return(o&1)==1;default:return e.alternate?a!==i||e.alternate.memoizedState!==e.memoizedState||e.alternate.ref!==e.ref:!0}},fe=e=>!!(e.flags&(se|8)||e.subtreeFlags&(se|8)),C=e=>{let t=[],n=[e];for(;n.length;){let e=n.pop();e&&(ce(e)&&fe(e)&&de(e)&&t.push(e),e.child&&n.push(e.child),e.sibling&&n.push(e.sibling))}return t},pe=e=>{switch(e.tag){case 18:return!0;case 7:case 6:case 23:case 22:return!0;case 3:return!1;default:{let t=typeof e.type==`object`&&e.type!==null?e.type.$$typeof:e.type;if(typeof t==`symbol`)return t.description===`react.concurrent_mode`||t.description===`react.async_mode`;switch(t){case ie:case ae:case oe:return!0;default:return!1}}}},me=e=>{let t=[],n=[];for(ce(e)?t.push(e):e.child&&n.push(e.child);n.length;){let e=n.pop();if(!e)break;ce(e)?t.push(e):e.child&&n.push(e.child),e.sibling&&n.push(e.sibling)}return t},he=(e,t,n=!1)=>{if(!e)return null;if(t(e)===!0)return e;let r=n?e.return:e.child;for(;r;){let e=he(r,t,n);if(e)return e;r=n?null:r.sibling}return null},ge=async(e,t,n=!1)=>{if(!e)return null;if(await t(e)===!0)return e;let r=n?e.return:e.child;for(;r;){let e=await ge(r,t,n);if(e)return e;r=n?null:r.sibling}return null},w=e=>{var t,n,r;let i=(t=e==null?void 0:e.actualDuration)==null?0:t,a=i,o=(n=e==null?void 0:e.child)==null?null:n;for(;i>0&&o!=null;)a-=(r=o.actualDuration)==null?0:r,o=o.sibling;return{selfTime:a,totalTime:i}},_e=e=>{var t;return!!((t=e.updateQueue)!=null&&t.memoCache)},ve=e=>{let t=e;return typeof t==`function`?t:typeof t==`object`&&t?ve(t.type||t.render):null},ye=e=>{let t=e;if(typeof t==`string`)return t;if(typeof t!=`function`&&!(typeof t==`object`&&t))return null;let n=t.displayName||t.name||null;if(n)return n;let r=ve(t);return r&&(r.displayName||r.name)||null},be=e=>{try{if(typeof e.version==`string`&&e.bundleType>0)return`development`}catch{}return`production`},xe=()=>{let e=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;return!!(e!=null&&e._instrumentationIsActive)||f(e)||h(e)},Se=new Set,Ce=0,we=new WeakMap,Te=(e,t=Ce++)=>{we.set(e,t)},Ee=e=>{let t=we.get(e);return!t&&e.alternate&&(t=we.get(e.alternate)),t||(t=Ce++,Te(e,t)),t},De=(e,t,n)=>{let r=t;for(;r!=null;){if(we.has(r)||Ee(r),!pe(r)&&de(r)&&e(r,`mount`),r.tag===13)if(r.memoizedState!==null){let t=r.child,n=t?t.sibling:null;if(n){let t=n.child;t!==null&&De(e,t,!1)}}else{let t=null;r.child!==null&&(t=r.child.child),t!==null&&De(e,t,!1)}else r.child!=null&&De(e,r.child,!0);r=n?r.sibling:null}},Oe=(e,t,n,r)=>{if(we.has(t)||Ee(t),!n)return;we.has(n)||Ee(n);let i=t.tag===13,a=!pe(t);a&&de(t)&&e(t,`update`);let o=i&&n.memoizedState!==null,s=i&&t.memoizedState!==null;if(o&&s){var c,l,u,d;let r=(c=(l=t.child)==null?void 0:l.sibling)==null?null:c,i=(u=(d=n.child)==null?void 0:d.sibling)==null?null:u;r!==null&&i!==null&&Oe(e,r,i,t)}else if(o&&!s){let n=t.child;n!==null&&De(e,n,!0)}else if(!o&&s){var f,p;Ae(e,n);let r=(f=(p=t.child)==null?void 0:p.sibling)==null?null:f;r!==null&&De(e,r,!0)}else if(t.child!==n.child){let n=t.child;for(;n;){if(n.alternate){let i=n.alternate;Oe(e,n,i,a?t:r)}else De(e,n,!1);n=n.sibling}}},ke=(e,t)=>{(t.tag===3||!pe(t))&&e(t,`unmount`)},Ae=(e,t)=>{var n,r,i,a;let o=t.tag===13&&t.memoizedState!==null,s=t.child;for(o&&(s=(n=(r=(i=(a=t.child)==null?void 0:a.sibling)==null?null:i)==null?void 0:r.child)==null?null:n);s!==null;)s.return!==null&&(ke(e,s),Ae(e,s)),s=s.sibling},je=0,Me=new WeakMap,Ne=(e,t)=>{let n=`current`in e?e.current:e,r=Me.get(e);r||(r={id:je++,prevFiber:null},Me.set(e,r));let{prevFiber:i}=r;if(!n)ke(t,n);else if(i!==null){let e=i&&i.memoizedState!=null&&i.memoizedState.element!=null&&i.memoizedState.isDehydrated!==!0,r=n.memoizedState!=null&&n.memoizedState.element!=null&&n.memoizedState.isDehydrated!==!0;!e&&r?De(t,n,!1):e&&r?Oe(t,n,n.alternate,null):e&&!r&&ke(t,n)}else De(t,n,!0);r.prevFiber=n},Pe=e=>Object.prototype.toString.call(e)===`[object Object]`&&(Object.getPrototypeOf(e)===Object.prototype||Object.getPrototypeOf(e)===null),Fe=(e,t=[])=>{if(!Pe(e))return[{path:t,value:e}];let n=[];for(let r in e){let i=e[r],a=t.concat(r);Pe(i)?n.push(...Fe(i,a)):n.push({path:a,value:i})}return n},Ie=new Set,Le=new Set,Re=new Set,ze=new Set,Be=new WeakMap,Ve=new WeakMap,He=e=>{var t;let n=(t=Be.get(e))==null?{}:t;if(Be.set(e,n),!n.onCommitFiberRoot||e.onCommitFiberRoot!==n.onCommitFiberRoot){let t=e.onCommitFiberRoot,r=(n,i,a)=>{var o;if(t==null||t(n,i,a),((o=Be.get(e))==null?void 0:o.onCommitFiberRoot)===r){Se.add(i),Ve.set(i,n);for(let e of Ie)e(n,i,a)}};n.onCommitFiberRoot=r,e.onCommitFiberRoot=r}if(!n.onCommitFiberUnmount||e.onCommitFiberUnmount!==n.onCommitFiberUnmount){let t=e.onCommitFiberUnmount,r=(n,i)=>{var a;if(t==null||t(n,i),((a=Be.get(e))==null?void 0:a.onCommitFiberUnmount)===r)for(let e of Le)e(n,i)};n.onCommitFiberUnmount=r,e.onCommitFiberUnmount=r}if(!n.onPostCommitFiberRoot||e.onPostCommitFiberRoot!==n.onPostCommitFiberRoot){let t=e.onPostCommitFiberRoot,r=(n,i)=>{var a;if(t==null||t(n,i),((a=Be.get(e))==null?void 0:a.onPostCommitFiberRoot)===r)for(let e of Re)e(n,i)};n.onPostCommitFiberRoot=r,e.onPostCommitFiberRoot=r}if(!n.onScheduleFiberRoot||e.onScheduleFiberRoot!==n.onScheduleFiberRoot){let t=e.onScheduleFiberRoot,r=(n,i,a)=>{var o;if(t==null||t(n,i,a),((o=Be.get(e))==null?void 0:o.onScheduleFiberRoot)===r)for(let e of ze)e(n,i,a)};n.onScheduleFiberRoot=r,e.onScheduleFiberRoot=r}},Ue=e=>{var t;let n=ee(e.onActive);n._instrumentationSource=(t=e.name)==null?s:t,He(n);let{onActive:r,onCommitFiberRoot:i,onCommitFiberUnmount:o,onPostCommitFiberRoot:c,onScheduleFiberRoot:l}=e;return i&&Ie.add(i),o&&Le.add(o),c&&Re.add(c),l&&ze.add(l),a(()=>{r&&g.delete(r),i&&Ie.delete(i),o&&Le.delete(o),c&&Re.delete(c),l&&ze.delete(l)})}})),Ge=r((()=>{x(),S(),We()})),Ke,qe=r((()=>{Ke=typeof window<`u`}));function Je(e){let t=String(e),n=t.length-1;return M.context.id+(n?String.fromCharCode(96+n):``)+t}function Ye(e){M.context=e}function Xe(){return{...M.context,id:M.getNextContextId(),count:0}}function Ze(e,t){let n=F,r=N,i=e.length===0,a=t===void 0?r:t,o=i?Qt:{owned:null,cleanups:null,context:a?a.context:null,owner:a},s=i?e:()=>e(()=>$e(()=>Ct(o)));N=o,F=null;try{return gt(s,!0)}finally{F=n,N=r}}function T(e,t){t=t?Object.assign({},qt,t):qt;let n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0};return[ut.bind(n),e=>(typeof e==`function`&&(e=P&&P.running&&P.sources.has(n)?e(n.tValue):e(n.value)),dt(n,e))]}function E(e,t,n){let r=mt(e,t,!1,Xt);$t&&P&&P.running?tn.push(r):ft(r)}function D(e,t,n){Yt=bt;let r=mt(e,t,!1,Xt),i=sn&&ct(sn);i&&(r.suspense=i),(!n||!n.render)&&(r.user=!0),nn?nn.push(r):ft(r)}function O(e,t,n){n=n?Object.assign({},qt,n):qt;let r=mt(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,$t&&P&&P.running?(r.tState=Xt,tn.push(r)):ft(r),ut.bind(r)}function Qe(e){return gt(e,!1)}function $e(e){if(!en&&F===null)return e();let t=F;F=null;try{return en?en.untrack(e):e()}finally{F=t}}function et(e,t,n){let r=Array.isArray(e),i,a=n&&n.defer;return n=>{let o;if(r){o=Array(e.length);for(let t=0;tt(o,i,n));return i=o,s}}function tt(e){D(()=>$e(e))}function k(e){return N===null||(N.cleanups===null?N.cleanups=[e]:N.cleanups.push(e)),e}function nt(e,t){Jt||(Jt=Symbol(`error`)),N=mt(void 0,void 0,!0),N.context={...N.context,[Jt]:[t]},P&&P.running&&P.sources.add(N);try{return e()}catch(e){Dt(e)}finally{N=N.owner}}function rt(){return F}function it(){return N}function at(e,t){let n=N,r=F;N=e,F=null;try{return gt(t,!0)}catch(e){Dt(e)}finally{N=n,F=r}}function ot(e){if(P&&P.running)return e(),P.done;let t=F,n=N;return Promise.resolve().then(()=>{F=t,N=n;let r;return($t||sn)&&(r=P||(P={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),r.done||(r.done=new Promise(e=>r.resolve=e)),r.running=!0),gt(e,!1),F=N=null,r?r.done:void 0})}function st(e,t){let n=Symbol(`context`);return{id:n,Provider:kt(n),defaultValue:e}}function ct(e){let t;return N&&N.context&&(t=N.context[e.id])!==void 0?t:e.defaultValue}function lt(e){let t=O(e),n=O(()=>Ot(t()));return n.toArray=()=>{let e=n();return Array.isArray(e)?e:e==null?[]:[e]},n}function ut(){let e=P&&P.running;if(this.sources&&(e?this.tState:this.state))if((e?this.tState:this.state)===Xt)ft(this);else{let e=tn;tn=null,gt(()=>xt(this),!1),tn=e}if(F){let e=this.observers;if(!e||e[e.length-1]!==F){let t=e?e.length:0;F.sources?(F.sources.push(this),F.sourceSlots.push(t)):(F.sources=[this],F.sourceSlots=[t]),e?(e.push(F),this.observerSlots.push(F.sources.length-1)):(this.observers=[F],this.observerSlots=[F.sources.length-1])}}return e&&P.sources.has(this)?this.tValue:this.value}function dt(e,t,n){let r=P&&P.running&&P.sources.has(e)?e.tValue:e.value;if(!e.comparator||!e.comparator(r,t)){if(P){let r=P.running;(r||!n&&P.sources.has(e))&&(P.sources.add(e),e.tValue=t),r||(e.value=t)}else e.value=t;e.observers&&e.observers.length&>(()=>{for(let t=0;t1e6)throw tn=[],Error()},!1)}return t}function ft(e){if(!e.fn)return;Ct(e);let t=rn;pt(e,P&&P.running&&P.sources.has(e)?e.tValue:e.value,t),P&&!P.running&&P.sources.has(e)&&queueMicrotask(()=>{gt(()=>{P&&(P.running=!0),F=N=e,pt(e,e.tValue,t),F=N=null},!1)})}function pt(e,t,n){let r,i=N,a=F;F=N=e;try{r=e.fn(t)}catch(t){return e.pure&&(P&&P.running?(e.tState=Xt,e.tOwned&&e.tOwned.forEach(Ct),e.tOwned=void 0):(e.state=Xt,e.owned&&e.owned.forEach(Ct),e.owned=null)),e.updatedAt=n+1,Dt(t)}finally{F=a,N=i}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&`observers`in e?dt(e,r,!0):P&&P.running&&e.pure?(P.sources.has(e)||(e.value=r),P.sources.add(e),e.tValue=r):e.value=r,e.updatedAt=n)}function mt(e,t,n,r=Xt,i){let a={fn:e,state:r,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:N,context:N?N.context:null,pure:n};if(P&&P.running&&(a.state=0,a.tState=r),N===null||N!==Qt&&(P&&P.running&&N.pure?N.tOwned?N.tOwned.push(a):N.tOwned=[a]:N.owned?N.owned.push(a):N.owned=[a]),en&&a.fn){let e=a.fn,[t,n]=T(void 0,{equals:!1}),r=en.factory(e,n);k(()=>r.dispose());let i,o=()=>ot(n).then(()=>{i&&(i.dispose(),i=void 0)});a.fn=n=>(t(),P&&P.running?(i||(i=en.factory(e,o)),i.track(n)):r.track(n))}return a}function ht(e){let t=P&&P.running;if((t?e.tState:e.state)===0)return;if((t?e.tState:e.state)===Zt)return xt(e);if(e.suspense&&$e(e.suspense.inFallback))return e.suspense.effects.push(e);let n=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt=0;r--){if(e=n[r],t){let t=e,i=n[r+1];for(;(t=t.owner)&&t!==i;)if(P.disposed.has(t))return}if((t?e.tState:e.state)===Xt)ft(e);else if((t?e.tState:e.state)===Zt){let t=tn;tn=null,gt(()=>xt(e,n[0]),!1),tn=t}}}function gt(e,t){if(tn)return e();let n=!1;t||(tn=[]),nn?n=!0:nn=[],rn++;try{let t=e();return _t(n),t}catch(e){n||(nn=null),tn=null,Dt(e)}}function _t(e){if(tn&&($t&&P&&P.running?yt(tn):vt(tn),tn=null),e)return;let t;if(P){if(!P.promises.size&&!P.queue.size){let e=P.sources,n=P.disposed;nn.push.apply(nn,P.effects),t=P.resolve;for(let e of nn)`tState`in e&&(e.state=e.tState),delete e.tState;P=null,gt(()=>{for(let e of n)Ct(e);for(let t of e){if(t.value=t.tValue,t.owned)for(let e=0,n=t.owned.length;eYt(n),!1),t&&t()}function vt(e){for(let t=0;t{r.delete(n),gt(()=>{P.running=!0,ht(n)},!1),P&&(P.running=!1)}))}}function bt(e){let t,n=0;for(t=0;t=0;t--)Ct(e.tOwned[t]);delete e.tOwned}if(P&&P.running&&e.pure)wt(e,!0);else if(e.owned){for(t=e.owned.length-1;t>=0;t--)Ct(e.owned[t]);e.owned=null}if(e.cleanups){for(t=e.cleanups.length-1;t>=0;t--)e.cleanups[t]();e.cleanups=null}P&&P.running?e.tState=0:e.state=0}function wt(e,t){if(t||(e.tState=0,P.disposed.add(e)),e.owned)for(let t=0;tn=$e(()=>(N.context={...N.context,[e]:t.value},lt(()=>t.children))),void 0),n}}function At(e){for(let t=0;t1?[]:null;return k(()=>At(a)),()=>{let c=e()||[],l=c.length,u,d;return c[Kt],$e(()=>{let e,t,p,m,h,g,_,v,y;if(l===0)o!==0&&(At(a),a=[],r=[],i=[],o=0,s&&(s=[])),n.fallback&&(r=[cn],i[0]=Ze(e=>(a[0]=e,n.fallback())),o=1);else if(o===0){for(i=Array(l),d=0;d=g&&v>=g&&r[_]===c[v];_--,v--)p[v]=i[_],m[v]=a[_],s&&(h[v]=s[_]);for(e=new Map,t=Array(v+1),d=v;d>=g;d--)y=c[d],u=e.get(y),t[d]=u===void 0?-1:u,e.set(y,d);for(u=g;u<=_;u++)y=r[u],d=e.get(y),d!==void 0&&d!==-1?(p[d]=i[u],m[d]=a[u],s&&(h[d]=s[u]),d=t[d],e.set(y,d)):a[u]();for(d=g;dAt(a)),()=>{let l=e()||[],u=l.length;return l[Kt],$e(()=>{if(u===0)return s!==0&&(At(a),a=[],r=[],i=[],s=0,o=[]),n.fallback&&(r=[cn],i[0]=Ze(e=>(a[0]=e,n.fallback())),s=1),i;for(r[0]===cn&&(a[0](),a=[],r=[],i=[],s=0),c=0;cl[c]):c>=r.length&&(i[c]=Ze(d));for(;ce(t||{}));return Ye(n),r}return $e(()=>e(t||{}))}function Nt(){return!0}function Pt(e){return(e=typeof e==`function`?e():e)?e:{}}function Ft(){for(let e=0,t=this.length;e=0;n--){let r=Pt(e[n])[t];if(r!==void 0)return r}},has(t){for(let n=e.length-1;n>=0;n--)if(t in Pt(e[n]))return!0;return!1},keys(){let t=[];for(let n=0;n=0;t--){let i=e[t];if(!i)continue;let a=Object.getOwnPropertyNames(i);for(let e=a.length-1;e>=0;e--){let t=a[e];if(t===`__proto__`||t===`constructor`)continue;let o=Object.getOwnPropertyDescriptor(i,t);if(!r[t])r[t]=o.get?{enumerable:!0,configurable:!0,get:Ft.bind(n[t]=[o.get.bind(i)])}:o.value===void 0?void 0:o;else{let e=n[t];e&&(o.get?e.push(o.get.bind(i)):o.value!==void 0&&e.push(()=>o.value))}}}let i={},a=Object.keys(r);for(let e=a.length-1;e>=0;e--){let t=a[e],n=r[t];n&&n.get?Object.defineProperty(i,t,n):i[t]=n?n.value:void 0}return i}function Lt(e,...t){let n=t.length;if(Gt&&Wt in e){let r=n>1?t.flat():t[0],i=t.map(t=>new Proxy({get(n){return t.includes(n)?e[n]:void 0},has(n){return t.includes(n)&&n in e},keys(){return t.filter(t=>t in e)}},un));return i.push(new Proxy({get(t){return r.includes(t)?void 0:e[t]},has(t){return r.includes(t)?!1:t in e},keys(){return Object.keys(e).filter(e=>!r.includes(e))}},un)),i}let r=[];for(let e=0;e<=n;e++)r[e]={};for(let i of Object.getOwnPropertyNames(e)){let a=n;for(let e=0;ee.fallback};return O(jt(()=>e.each,e.children,t||void 0))}function zt(e){let t=`fallback`in e&&{fallback:()=>e.fallback};return O(Mt(()=>e.each,e.children,t||void 0))}function j(e){let t=e.keyed,n=O(()=>e.when,void 0,void 0),r=t?n:O(n,void 0,{equals:(e,t)=>!e==!t});return O(()=>{let i=r();if(i){let a=e.children;return typeof a==`function`&&a.length>0?$e(()=>a(t?i:()=>{if(!$e(r))throw dn(`Show`);return n()})):a}return e.fallback},void 0,void 0)}function Bt(e){let t=lt(()=>e.children),n=O(()=>{let e=t(),n=Array.isArray(e)?e:[e],r=()=>void 0;for(let e=0;ea()?void 0:i.when,void 0,void 0),s=i.keyed?o:O(o,void 0,{equals:(e,t)=>!e==!t});r=()=>a()||(s()?[t,o,i]:void 0)}return r});return O(()=>{let t=n()();if(!t)return e.fallback;let[r,i,a]=t,o=a.children;return typeof o==`function`&&o.length>0?$e(()=>o(a.keyed?i():()=>{var e;if(((e=$e(n)())==null?void 0:e[0])!==r)throw dn(`Match`);return i()})):o},void 0,void 0)}function Vt(e){return e}function Ht(e){let t;M.context&&M.load&&(t=M.load(M.getContextId()));let[n,r]=T(t,void 0);return fn||(fn=new Set),fn.add(r),k(()=>fn.delete(r)),O(()=>{let t;if(t=n()){let n=e.fallback;return typeof n==`function`&&n.length?$e(()=>n(t,()=>r())):n}return nt(()=>e.children,r)},void 0,void 0)}var M,Ut,Wt,Gt,Kt,qt,Jt,Yt,Xt,Zt,Qt,N,P,$t,en,F,tn,nn,rn,an,on,sn,cn,ln,un,dn,fn,I=r((()=>{M={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return Je(this.context.count)},getNextContextId(){return Je(this.context.count++)}},Ut=(e,t)=>e===t,Wt=Symbol(`solid-proxy`),Gt=typeof Proxy==`function`,Kt=Symbol(`solid-track`),qt={equals:Ut},Jt=null,Yt=vt,Xt=1,Zt=2,Qt={owned:null,cleanups:null,context:null,owner:null},N=null,P=null,$t=null,en=null,F=null,tn=null,nn=null,rn=0,[an,on]=T(!1),cn=Symbol(`fallback`),ln=!1,un={get(e,t,n){return t===Wt?n:e.get(t)},has(e,t){return t===Wt?!0:e.has(t)},set:Nt,deleteProperty:Nt,getOwnPropertyDescriptor(e,t){return{configurable:!0,enumerable:!0,get(){return e.get(t)},set:Nt,deleteProperty:Nt}},ownKeys(e){return e.keys()}},dn=e=>`Stale read from <${e}>.`}));function pn(e,t){return t-e}function mn(e){let t=e[0].name,n=e.length,r=Math.min(4,n);for(let n=1;n{qe(),vn=e=>{let t=``,n=new Map;for(let t of e){let{forget:e,time:r,aggregatedCount:i,name:a}=t;n.has(i)||n.set(i,[]);let o=n.get(i);o&&o.push({name:a,forget:e,time:r==null?0:r})}let r=Array.from(n.keys()).sort(pn),i=[],a=0;for(let e of r){let t=n.get(e);if(!t)continue;let r=mn(t),o=hn(t),s=gn(t);a+=o,t.length>4&&(r+=`…`),e>1&&(r+=` × ${e}`),s&&(r=`✨${r}`),i.push(r)}return t=i.join(`, `),t.length?(t.length>40&&(t=`${t.slice(0,40)}…`),a>=.01&&(t+=` (${Number(a.toFixed(2))}ms)`),t):null},yn=()=>Ke?(window.reactScanIdCounter===void 0&&(window.reactScanIdCounter=0),`${++window.reactScanIdCounter}`):`0`,bn=e=>{let t=e.createOscillator(),n=e.createGain();t.connect(n),n.connect(e.destination);let r={type:`sine`,freq:[392,600],duration:.3,gain:.12},i=r.freq,a=r.duration/i.length;i.forEach((n,r)=>{t.frequency.setValueAtTime(n,e.currentTime+r*a)}),t.type=r.type,n.gain.setValueAtTime(r.gain,e.currentTime),n.gain.setTargetAtTime(0,e.currentTime+r.duration*.7,.05),t.start(),t.stop(e.currentTime+r.duration)}})),Sn,Cn=r((()=>{I(),Sn=e=>{let[t,n]=T(e),r=new Set,i=e=>{let i=t();return Object.is(i,e)?i:(n(()=>e),r.forEach(t=>t(e)),e)};return{get:t,set:i,facade:{get value(){return t()},set value(e){i(e)},subscribe:e=>(r.add(e),()=>{r.delete(e)})}}}})),wn,Tn,En,Dn,On,L,kn,An,jn,Mn,R,Nn,Pn,Fn,In=r((()=>{Cn(),qe(),wn=Sn(!0),Tn=Sn(Ke&&window.self!==window.top),En=Sn({kind:`uninitialized`}),Dn=Sn(0),On=Sn({enabled:!0,log:!1,showToolbar:!0,animationSpeed:`fast`,dangerouslyForceRunInProduction:!1,showFPS:!0,showNotificationCount:!0,allowInIframe:!1}),L=En.get,kn=En.set,An=Dn.get,jn=Dn.set,Mn=Tn.get,R=On.get,Nn=On.set,Pn=On.facade,Fn={wasDetailsOpen:wn.facade,isInIframe:Tn.facade,inspectState:En.facade,fiberRoots:new Set,reportData:new Map,legacyReportData:new Map,lastReportTime:Dn.facade,interactionListeningForRenders:null,changesListeners:new Map}})),Ln,Rn,zn=r((()=>{Ln=e=>e(),Rn=class e extends Array{constructor(e=25){super(),this.capacity=e}push(...e){let t=super.push(...e);for(;this.length>this.capacity;)this.shift();return t}static fromArray(t,n){let r=new e(n);return r.push(...t),r}}}));function Bn(e,n=0){if(n<0)return`…`;switch(typeof e){case`function`:return e.toString();case`string`:return e;case`number`:case`boolean`:case`undefined`:return String(e);case`object`:break;default:return String(e)}if(e===null)return`null`;if(er.has(e)){let t=er.get(e);if(t!==void 0)return t}if(Array.isArray(e)){let t=e.length?`[${e.length}]`:`[]`;return er.set(e,t),t}if((0,t.isValidElement)(e)){var r;let t=`<${(r=ye(e.type))==null?``:r} ${e.props?Object.keys(e.props).length:0}>`;return er.set(e,t),t}if(Object.getPrototypeOf(e)===Object.prototype){let t=Object.keys(e),n=t.length?`{${t.length}}`:`{}`;return er.set(e,n),n}let i=e&&typeof e==`object`?e.constructor:void 0;if(i&&typeof i==`function`&&i.name){let t=`${i.name}{…}`;return er.set(e,t),t}let a=`${Object.prototype.toString.call(e).slice(8,-1)}{…}`;return er.set(e,a),a}function Vn(e,t){var n;if(!e||!t)return;let r=e.memoizedValue,i={type:4,name:(n=e.context.displayName)==null?`Context.Provider`:n,value:r,contextType:ir(e.context)};this.push(i)}function Hn(e){return String(Ee(e))}function Un(e){let t=Hn(e),n=ur.get(ve(e));if(n)return n.get(t)}function Wn(e,t){let n=ve(e.type),r=Hn(e),i=ur.get(n);i||(i=new Map,ur.set(n,i)),i.set(r,t)}var Gn,Kn,qn,Jn,Yn,Xn,Zn,Qn,$n,er,tr,nr,rr,ir,ar,or,sr,cr,lr,ur,dr,fr,pr=r((()=>{Cn(),Ge(),xn(),Lr(),In(),Gn={mount:1,update:2,unmount:4},Kn=0,qn=performance.now(),Jn=0,Yn=!1,Xn=()=>{Jn++;let e=performance.now();e-qn>=1e3&&(Kn=Jn,Jn=0,qn=e),requestAnimationFrame(Xn)},Zn=()=>(Yn||(Yn=!0,Xn(),Kn=60),Kn),Qn=(e,t)=>Bn(e)===Bn(t)&&$n.includes(typeof e)&&$n.includes(typeof t),$n=[`function`,`object`],er=new WeakMap,tr=e=>{if(!e)return[];let t=[];if(e.tag===0||e.tag===11||e.tag===15||e.tag===14){var n;let r=e.memoizedState,i=(n=e.alternate)==null?void 0:n.memoizedState,a=0;for(;r;){if(r.queue&&r.memoizedState!==void 0){let e={type:2,name:a.toString(),value:r.memoizedState,prevValue:i==null?void 0:i.memoizedState};_n(e.prevValue,e.value)||t.push(e)}r=r.next,i=i==null?void 0:i.next,a++}return t}if(e.tag===1){var r;let n={type:3,name:`state`,value:e.memoizedState,prevValue:(r=e.alternate)==null?void 0:r.memoizedState};return _n(n.prevValue,n.value)||t.push(n),t}return t},nr=0,rr=new WeakMap,ir=e=>rr.get(e)||(nr++,rr.set(e,nr),nr),ar=e=>{let t=[];return ue(e,Vn.bind(t)),t},or=new Map,sr=!1,cr=()=>Array.from(or.values()),lr=16,ur=new WeakMap,dr=(e,t,n,r,i)=>{let a=Date.now(),o=Un(e);if((r||i)&&(!o||a-(o.lastRenderTimestamp||0)>lr)){let r=o||{selfTime:0,totalTime:0,renderCount:0,lastRenderTimestamp:a};r.renderCount=(r.renderCount||0)+1,r.selfTime=t||0,r.totalTime=n||0,r.lastRenderTimestamp=a,Wn(e,{...r})}},fr=(e,t)=>{let n=Sn(!R().enabled),r={isPaused:n.facade,getIsPaused:n.get,setIsPaused:n.set,fiberRoots:new WeakSet};return or.set(e,{key:e,config:t,instrumentation:r}),sr||(sr=!0,Ue({name:`react-scan`,onActive:t.onActive,onCommitFiberRoot(e,t){r.fiberRoots.add(t);let n=cr();for(let e of n)e.config.onCommitStart();Ne(t.current,(e,t)=>{let n=ve(e.type);if(!n)return null;let r=cr(),i=[];for(let t=0,n=r.length;te.config.trackChanges)){let t=Ar(e).changes,n=jr(e).changes,r=Mr(e).changes;a.push.apply(null,t.map(e=>({type:1,name:e.name,value:e.value})));for(let t of n)e.tag===1?a.push({type:3,name:t.name.toString(),value:t.value}):a.push({type:2,name:t.name.toString(),value:t.value});a.push.apply(null,r.map(e=>({type:4,name:e.name,value:e.value,contextType:Number(e.contextType)})))}let{selfTime:o,totalTime:s}=w(e),c=Zn(),l={phase:Gn[t],componentName:ye(n),count:1,changes:a,time:o,forget:_e(e),unnecessary:null,didCommit:fe(e),fps:c},u=a.length>0,d=C(e).length>0;t===`update`&&dr(e,o,s,u,d);for(let t=0,n=i.length;t{Ge(),pr(),xn(),mr=e=>{if(`__REACT_DEVTOOLS_GLOBAL_HOOK__`in window){let n=window.__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!(n!=null&&n.renderers))return null;for(let[,r]of Array.from(n.renderers))try{var t;let n=(t=r.findFiberByHostInstance)==null?void 0:t.call(r,e);if(n)return n}catch{}}if(`_reactRootContainer`in e){var n,r;return(n=(r=e._reactRootContainer)==null||(r=r._internalRoot)==null||(r=r.current)==null?void 0:r.child)==null?null:n}for(let t in e)if(t.startsWith(`__reactInternalInstance$`)||t.startsWith(`__reactFiber`))return e[t];return null},hr=e=>{let t=e,n=null;for(;t;){if(le(t))return[t,n];ce(t)&&!n&&(n=t),t=t.return}return null},gr=e=>{var t,n,r;let i=(t=e.memoizedProps)==null?{}:t,a=(n=(r=e.alternate)==null?void 0:r.memoizedProps)==null?{}:n,o=[];for(let e in i){if(e===`children`)continue;let t=i[e],n=a[e];_n(t,n)||o.push({name:e,value:t,prevValue:n,type:1})}return o},_r=e=>e instanceof Promise||typeof e==`object`&&!!e&&`then`in e})),yr,br,xr,Sr,Cr,wr,Tr,Er,Dr,Or,kr,Ar,jr,Mr,Nr,Pr,Fr,Ir,Lr=r((()=>{Ge(),xn(),vr(),yr=new Map,br=new Map,xr=new Map,Sr=null,Cr=/\[(?\w+),\s*set\w+\]/g,wr=()=>({current:[],changes:new Set,changesCounts:new Map}),Tr=e=>{var t,n;let r=((t=e.type)==null||(n=t.toString)==null?void 0:n.call(t))||``;return r?Array.from(r.matchAll(Cr),e=>{var t,n;return(t=(n=e.groups)==null?void 0:n.name)==null?``:t}):[]},Er=()=>{yr.clear(),br.clear(),xr.clear(),Sr=null},Dr=e=>{let t=e.type!==Sr;return Sr=e.type,t},Or=(e,t,n,r)=>{let i=e.get(t),a=e===yr||e===xr,o=!_n(n,r);if(!i)return e.set(t,{count:o&&a?1:0,currentValue:n,previousValue:r,lastUpdated:Date.now()}),{hasChanged:o,count:o&&a?1:+!a};if(!_n(i.currentValue,n)){let r=i.count+1;return e.set(t,{count:r,currentValue:n,previousValue:i.currentValue,lastUpdated:Date.now()}),{hasChanged:!0,count:r}}return{hasChanged:!1,count:i.count}},kr=e=>{if(!e)return{};if(e.tag===0||e.tag===11||e.tag===15||e.tag===14){let t=e.memoizedState,n={},r=0;for(;t;)t.queue&&t.memoizedState!==void 0&&(n[r]=t.memoizedState),t=t.next,r++;return n}return e.tag===1&&e.memoizedState||{}},Ar=e=>{var t;let n=e.memoizedProps||{},r=((t=e.alternate)==null?void 0:t.memoizedProps)||{},i={},a={};for(let e of Object.keys(n))e in n&&(i[e]=n[e],a[e]=r[e]);return{current:i,prev:a,changes:gr(e).map(e=>({name:e.name,value:e.value,prevValue:e.prevValue}))}},jr=e=>{let t=kr(e),n=e.alternate?kr(e.alternate):{},r=[];for(let[i,a]of Object.entries(t)){let t=e.tag===1?i:Number(i);e.alternate&&!_n(n[i],a)&&r.push({name:t,value:a,prevValue:n[i]})}return{current:t,prev:n,changes:r}},Mr=e=>{let t=Fr(e),n=e.alternate?Fr(e.alternate):new Map,r={},i={},a=[],o=new Set;for(let[e,s]of t){let t=s.displayName;if(o.has(e))continue;o.add(e),r[t]=s.value;let c=n.get(e);c&&(i[t]=c.value,_n(c.value,s.value)||a.push({name:t,value:s.value,prevValue:c.value,contextType:e}))}return{current:r,prev:i,changes:a}},Nr=e=>{if(!e)return{data:{fiberProps:wr(),fiberState:wr(),fiberContext:wr()},shouldUpdate:!1};let t=!1,n=Dr(e),r=wr();if(e.memoizedProps){let{current:n,changes:i}=Ar(e);for(let[e,t]of Object.entries(n))r.current.push({name:e,value:_r(t)?{type:`promise`,displayValue:`Promise`}:t});for(let e of i){let{hasChanged:n,count:i}=Or(yr,e.name,e.value,e.prevValue);n&&(t=!0,r.changes.add(e.name),r.changesCounts.set(e.name,i))}}let i=wr(),{current:a,changes:o}=jr(e);for(let[t,n]of Object.entries(a)){let r=e.tag===1?t:Number(t);i.current.push({name:r,value:n})}for(let e of o){let{hasChanged:n,count:r}=Or(br,e.name,e.value,e.prevValue);n&&(t=!0,i.changes.add(e.name),i.changesCounts.set(e.name,r))}let s=wr(),{current:c,changes:l}=Mr(e);for(let[e,t]of Object.entries(c))s.current.push({name:e,value:t});if(!n)for(let e of l){let{hasChanged:n,count:r}=Or(xr,e.name,e.value,e.prevValue);n&&(t=!0,s.changes.add(e.name),s.changesCounts.set(e.name,r))}return!t&&!n&&(r.changes.clear(),i.changes.clear(),s.changes.clear()),{data:{fiberProps:r,fiberState:i,fiberContext:s},shouldUpdate:t||n}},Pr=new WeakMap,Fr=e=>{if(!e)return new Map;let t=Pr.get(e);if(t)return t;let n=new Map,r=e;for(;r;){let e=r.dependencies;if(e!=null&&e.firstContext){let t=e.firstContext;for(;t;){var i;let e=t.memoizedValue,r=(i=t.context)==null?void 0:i.displayName;if(n.has(e)||n.set(t.context,{value:e,displayName:r==null?`UnnamedContext`:r,contextType:null}),t===t.next)break;t=t.next}}r=r.return}return Pr.set(e,n),n},Ir=e=>{if(!e)return{fiberProps:wr(),fiberState:wr(),fiberContext:wr()};let t=wr();if(e.memoizedProps){let{current:n,changes:r}=Ar(e);for(let[e,r]of Object.entries(n))t.current.push({name:e,value:_r(r)?{type:`promise`,displayValue:`Promise`}:r});for(let e of r)t.changes.add(e.name),t.changesCounts.set(e.name,1)}let n=wr();if(e.memoizedState){let{current:t,changes:r}=jr(e);for(let[e,r]of Object.entries(t))n.current.push({name:e,value:_r(r)?{type:`promise`,displayValue:`Promise`}:r});for(let e of r)n.changes.add(e.name),n.changesCounts.set(e.name,1)}let r=wr(),{current:i,changes:a}=Mr(e);for(let[e,t]of Object.entries(i))r.current.push({name:e,value:_r(t)?{type:`promise`,displayValue:`Promise`}:t});for(let e of a)r.changes.add(e.name),r.changesCounts.set(e.name,1);return{fiberProps:t,fiberState:n,fiberContext:r}}})),Rr,zr,Br,Vr,Hr,Ur,Wr,Gr,Kr,qr,Jr,Yr,Xr,Zr,Qr,$r,ei,ti,ni,ri,ii,ai,oi,si,ci,li,ui,di,fi,pi,mi,hi,gi,_i=r((()=>{Ge(),In(),zn(),Lr(),vr(),xn(),Rr={skipProviders:!0,skipHocs:!0,skipContainers:!0,skipMinified:!0,skipUtilities:!0,skipBoundaries:!0},zr={providers:[/Provider$/,/^Provider$/,/^Context$/],hocs:[/^with[A-Z]/,/^forward(?:Ref)?$/i,/^Forward(?:Ref)?\(/],containers:[/^(?:App)?Container$/,/^Root$/,/^ReactDev/],utilities:[/^Fragment$/,/^Suspense$/,/^ErrorBoundary$/,/^Portal$/,/^Consumer$/,/^Layout$/,/^Router/,/^Hydration/],boundaries:[/^Boundary$/,/Boundary$/,/^Provider$/,/Provider$/]},Br=(e,t=Rr)=>{let n=[];return t.skipProviders&&n.push(...zr.providers),t.skipHocs&&n.push(...zr.hocs),t.skipContainers&&n.push(...zr.containers),t.skipUtilities&&n.push(...zr.utilities),t.skipBoundaries&&n.push(...zr.boundaries),!n.some(t=>t.test(e))},Vr=[/^[a-z]$/,/^[a-z][0-9]$/,/^_+$/,/^[A-Za-z][_$]$/,/^[a-z]{1,2}$/],Hr=e=>{var t,n;for(let t=0;te.length/2,a=/^[a-z]+$/.test(e),o=/[$_]{2,}/.test(e);return Number(r)+Number(i)+Number(a)+Number(o)>=2},Ur=e=>{let t=ye(e);return t?t.replace(/^(?:Memo|Forward(?:Ref)?|With.*?)\((?.*?)\)$/,`$`):``},Wr=(e,t=Rr)=>{if(!e||!ye(e.type))return[];let n=[],r=e;for(;r.return;){let e=Ur(r.type);e&&!Hr(e)&&Br(e,t)&&e.toLowerCase()!==e&&n.push(e),r=r.return}let i=Array(n.length);for(let e=0;e!0)=>{let n=e;for(;n;){let e=ye(n.type);if(e&&t(e))return e;n=n.return}return null},qr=`never-hidden`,Jr=()=>{Kr==null||Kr();let e=()=>{document.hidden&&(qr=Date.now())};document.addEventListener(`visibilitychange`,e),Kr=()=>{document.removeEventListener(`visibilitychange`,e)}},Yr=50,Xr=new Rn(Yr),Zr=new Set,Qr=e=>{Xr.push(e),Zr.forEach(t=>t(e))},$r=e=>(Xr.forEach(t=>e(t)),Zr.add(e),()=>{Zr.delete(e)}),ei=()=>Xr.length>0,ti=()=>{Xr=new Rn(Yr)},ni=e=>[`pointerup`,`click`].includes(e)?`pointer`:(e.includes(`key`),[`keydown`,`keyup`].includes(e)?`keyboard`:null),ri=null,ii=e=>{Jr();let t=new Map,n=new Map,r=r=>{if(!r.interactionId)return;if(r.interactionId&&r.target&&!n.has(r.interactionId)&&n.set(r.interactionId,r.target),r.target){let e=r.target;for(;e;){if(e.id===`react-scan-toolbar-root`||e.id===`react-scan-root`)return;e=e.parentElement}}let i=t.get(r.interactionId);if(i)r.duration>i.latency?(i.entries=[r],i.latency=r.duration):r.duration===i.latency&&r.startTime===i.entries[0].startTime&&i.entries.push(r);else{let n=ni(r.name);if(!n)return;let i={id:r.interactionId,latency:r.duration,entries:[r],target:r.target,type:n,startTime:r.startTime,endTime:Date.now(),processingStart:r.processingStart,processingEnd:r.processingEnd,duration:r.duration,inputDelay:r.processingStart-r.startTime,processingDuration:r.processingEnd-r.processingStart,presentationDelay:r.duration-(r.processingEnd-r.startTime),timestamp:Date.now(),timeSinceTabInactive:qr===`never-hidden`?`never-hidden`:Date.now()-qr,visibilityState:document.visibilityState,timeOrigin:performance.timeOrigin,referrer:document.referrer};t.set(i.id,i),ri||(ri=requestAnimationFrame(()=>{requestAnimationFrame(()=>{e(t.get(i.id)),ri=null})}))}},i=new PerformanceObserver(e=>{let t=e.getEntries();for(let e=0,n=t.length;ei.disconnect()},ai=()=>ii(e=>{Qr({kind:`entry-received`,entry:e})}),oi=new Rn(25),si=(e,t)=>{let n=null;for(let r of t){if(r.type!==e.type)continue;if(n===null){n=r;continue}let t=(e,t)=>Math.abs(e.startDateTime)-(t.startTime+t.timeOrigin);t(r,e)$r(e=>{let t=e.kind===`auto-complete-race`?oi.find(t=>t.interactionUUID===e.interactionUUID):si(e.entry,oi);t&&t.completeInteraction(e)}),li=({onMicroTask:e,onRAF:t,onTimeout:n,abort:r})=>{queueMicrotask(()=>{(r==null?void 0:r())!==!0&&e()&&requestAnimationFrame(()=>{(r==null?void 0:r())!==!0&&t()&&setTimeout(()=>{(r==null?void 0:r())!==!0&&n()},0)})})},ui=e=>{let t=mr(e);if(!t)return;let n=t?ye(t==null?void 0:t.type):`N/A`;if(!n){var r;n=(r=Gr(t,e=>e.length>2))==null?`N/A`:r}if(n)return{componentPath:Wr(t),childrenTree:{},componentName:n,elementFiber:t}},di=(e,t)=>{let n=null,r=t=>{switch(e){case`pointer`:return t.phase===`start`?`pointerup`:t.target instanceof HTMLInputElement||t.target instanceof HTMLSelectElement?`change`:`click`;case`keyboard`:return t.phase===`start`?`keydown`:`change`}},i={current:{kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:Date.now(),interactionType:e}},a=n=>{var a;if(n.composedPath().some(e=>e instanceof Element&&e.id===`react-scan-toolbar-root`)||(Date.now()-i.current.stageStart>2e3&&(i.current={kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:Date.now(),interactionType:e}),i.current.kind!==`uninitialized-stage`))return;let s=performance.now();t==null||(a=t.onStart)==null||a.call(t,i.current.interactionUUID);let c=ui(n.target);if(!c){var l;t==null||(l=t.onError)==null||l.call(t,i.current.interactionUUID);return}let u={},d=mi(u);i.current={...i.current,interactionType:e,blockingTimeStart:Date.now(),childrenTree:c.childrenTree,componentName:c.componentName,componentPath:c.componentPath,fiberRenders:u,kind:`interaction-start`,interactionStartDetail:s,stopListeningForRenders:d};let f=r({phase:`end`,target:n.target});document.addEventListener(f,o,{once:!0}),requestAnimationFrame(()=>{document.removeEventListener(f,o)})};document.addEventListener(r({phase:`start`}),a,{capture:!0});let o=(r,a,o)=>{if(i.current.kind!==`interaction-start`&&a===n){var s;if(e===`pointer`&&r.target instanceof HTMLSelectElement){i.current={kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:Date.now(),interactionType:e};return}t==null||(s=t.onError)==null||s.call(t,i.current.interactionUUID),i.current={kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:Date.now(),interactionType:e};return}n=a,li({abort:o,onMicroTask:()=>i.current.kind===`uninitialized-stage`?!1:(i.current={...i.current,kind:`js-end-stage`,jsEndDetail:performance.now()},!0),onRAF:()=>{if(i.current.kind!==`js-end-stage`&&i.current.kind!==`raf-stage`){var n;return t==null||(n=t.onError)==null||n.call(t,i.current.interactionUUID),i.current={kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:Date.now(),interactionType:e},!1}return i.current={...i.current,kind:`raf-stage`,rafStart:performance.now()},!0},onTimeout:()=>{if(i.current.kind!==`raf-stage`){var n;t==null||(n=t.onError)==null||n.call(t,i.current.interactionUUID),i.current={kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:Date.now(),interactionType:e};return}let r=Date.now(),a=Object.freeze({...i.current,kind:`timeout-stage`,blockingTimeEnd:r,commitEnd:performance.now()});i.current={kind:`uninitialized-stage`,interactionUUID:yn(),stageStart:r,interactionType:e};let o=!1,s=e=>{var n;o=!0;let r={detailedTiming:a,latency:e.kind===`auto-complete-race`?e.detailedTiming.commitEnd-e.detailedTiming.interactionStartDetail:e.entry.latency,completedAt:Date.now(),flushNeeded:!0};t==null||(n=t.onComplete)==null||n.call(t,a.interactionUUID,r,e);let i=oi.filter(e=>e.interactionUUID!==a.interactionUUID);return oi=Rn.fromArray(i,25),r},c={completeInteraction:s,endDateTime:Date.now(),startDateTime:a.blockingTimeStart,type:e,interactionUUID:a.interactionUUID};if(oi.push(c),pi())setTimeout(()=>{if(o)return;s({kind:`auto-complete-race`,detailedTiming:a,interactionUUID:a.interactionUUID});let e=oi.filter(e=>e.interactionUUID!==a.interactionUUID);oi=Rn.fromArray(e,25)},1e3);else{let e=oi.filter(e=>e.interactionUUID!==a.interactionUUID);oi=Rn.fromArray(e,25),s({kind:`auto-complete-race`,detailedTiming:a,interactionUUID:a.interactionUUID})}}})},s=e=>{let t=yn();o(e,t,()=>t!==n)};return e===`keyboard`&&document.addEventListener(`keypress`,s),()=>{document.removeEventListener(r({phase:`start`}),a,{capture:!0}),document.removeEventListener(`keypress`,s)}},fi=e=>{var t;return(t=re(e,e=>{if(ce(e))return!0}))==null?void 0:t.stateNode},pi=()=>`PerformanceEventTiming`in globalThis,mi=e=>{let t=t=>{var n,r,i,a,o;let s=ye(t.type);if(!s)return;let c=e[s];if(!c){var l;let n=new Set,r=t.return&&hr(t.return),i=r&&ye(r[0]);i&&n.add(i);let{selfTime:a,totalTime:o}=w(t),c=Ir(t),u={current:[],changes:new Set,changesCounts:new Map},d={fiberProps:c.fiberProps||u,fiberState:c.fiberState||u,fiberContext:c.fiberContext||u};e[s]={renderCount:1,hasMemoCache:_e(t),wasFiberRenderMount:gi(t),parents:n,selfTime:a,totalTime:o,nodeInfo:[{element:fi(t),name:(l=ye(t.type))==null?`Unknown`:l,selfTime:w(t).selfTime}],changes:d};return}if(!((n=hr(t))==null||(n=n[0])==null)&&n.type){let e=t.return&&hr(t.return),n=e&&ye(e[0]);n&&c.parents.add(n)}let{selfTime:u,totalTime:d}=w(t),f=Ir(t);if(!f)return;let p={current:[],changes:new Set,changesCounts:new Map};c.wasFiberRenderMount=c.wasFiberRenderMount||gi(t),c.hasMemoCache=c.hasMemoCache||_e(t),c.changes={fiberProps:hi(((r=c.changes)==null?void 0:r.fiberProps)||p,f.fiberProps||p),fiberState:hi(((i=c.changes)==null?void 0:i.fiberState)||p,f.fiberState||p),fiberContext:hi(((a=c.changes)==null?void 0:a.fiberContext)||p,f.fiberContext||p)},c.renderCount+=1,c.selfTime+=u,c.totalTime+=d,c.nodeInfo.push({element:fi(t),name:(o=ye(t.type))==null?`Unknown`:o,selfTime:w(t).selfTime})};return Fn.interactionListeningForRenders=t,()=>{Fn.interactionListeningForRenders===t&&(Fn.interactionListeningForRenders=null)}},hi=(e,t)=>{let n={current:[...e.current],changes:new Set,changesCounts:new Map};for(let e of t.current)n.current.some(t=>t.name===e.name)||n.current.push(e);for(let r of t.changes)if(typeof r==`string`||typeof r==`number`){n.changes.add(r);let i=e.changesCounts.get(r)||0,a=t.changesCounts.get(r)||0;n.changesCounts.set(r,i+a)}return n},gi=e=>{if(!e.alternate)return!0;let t=e.alternate,n=t&&t.memoizedState!=null&&t.memoizedState.element!=null&&t.memoizedState.isDehydrated!==!0,r=e.memoizedState!=null&&e.memoizedState.element!=null&&e.memoizedState.isDehydrated!==!0;return!n&&r}}));function vi(){let e,t;function n(){let r=null;yi=null,yi={},r=mi(yi);let i=performance.timeOrigin,a=performance.now();return e=requestAnimationFrame(()=>{t=setTimeout(()=>{let e=performance.now(),t=e-a,o=performance.timeOrigin;Ni.push(e+o);let s=Ni.filter(t=>e+o-t<=1e3),c=s.length;Ni=s;let l=Di!==null&&Oi!==null?e+o-(Oi+Di)<100:null,u=Ai!==null&&Ai;if(t>150&&!l&&document.visibilityState===`visible`&&!u){let n=o+e,r=a+i;Ei({kind:`long-render`,id:yn(),data:{endAt:n,startAt:r,meta:{fiberRenders:yi,latency:t,fps:c}}})}Di=null,Oi=null,r==null||r(),n()},0)}),r}let r=n();return()=>{r(),cancelAnimationFrame(e),clearTimeout(t)}}var yi,bi,xi,Si,Ci,wi,Ti,Ei,Di,Oi,ki,Ai,ji,Mi,Ni,Pi,Fi=r((()=>{xn(),_i(),zn(),yi=null,bi=200,xi=new Rn(bi),Si=new Set,Ci=()=>xi,wi=e=>(Si.add(e),()=>{Si.delete(e)}),Ti=()=>{xi=new Rn(bi),Si.forEach(e=>e())},Ei=e=>{let t=[...xi,e],n=(e,n)=>{let r=t.find(t=>t.kind===`long-render`||t.id===e.id?!1:e.data.startAt<=t.data.startAt&&e.data.endAt<=t.data.endAt&&e.data.endAt>=t.data.startAt||t.data.startAt<=e.data.startAt&&t.data.endAt>=e.data.startAt||e.data.startAt<=t.data.startAt&&e.data.endAt>=t.data.endAt);r&&n(r)},r=new Set;t.forEach(e=>{e.kind!==`interaction`&&n(e,()=>{r.add(e.id)})}),xi=Rn.fromArray(t.filter(e=>!r.has(e.id)),bi),Si.forEach(e=>e())},Di=null,Oi=null,ki=null,ji=()=>{let e=e=>{Ai=e.composedPath().map(e=>e.id).filter(Boolean).includes(`react-scan-toolbar`)};return document.addEventListener(`mouseover`,e),ki=e,()=>{ki&&document.removeEventListener(`mouseover`,ki)}},Mi=()=>{let e=()=>{Di=performance.now(),Oi=performance.timeOrigin};return document.addEventListener(`visibilitychange`,e),()=>{document.removeEventListener(`visibilitychange`,e)}},Ni=[],Pi=()=>{let e=ai(),t=ji(),n=Mi(),r=vi(),i=async(e,t,n)=>{Ei({kind:`interaction`,id:yn(),data:{startAt:t.detailedTiming.blockingTimeStart,endAt:performance.now()+performance.timeOrigin,meta:{...t,kind:n.kind}}}),t.detailedTiming.stopListeningForRenders(),ei()&&ti()},a=di(`pointer`,{onComplete:i}),o=di(`keyboard`,{onComplete:i}),s=ci();return()=>{t(),n(),r(),e(),a(),s(),o()}}}));function Ii(){Ri&&(cancelAnimationFrame(Ri),Ri=null),z!=null&&z.parentNode&&z.parentNode.removeChild(z),z=null,Li=null}var z,Li,Ri,zi,Bi,Vi,Hi,Ui,Wi,Gi,Ki,qi,Ji,Yi,Xi=r((()=>{I(),zn(),z=null,Li=null,Ri=null,[zi,Bi]=T({kind:`idle`,current:null}),Vi=e=>{Bi(e),requestAnimationFrame(()=>{qi()})},Hi=null,Ui=0,Wi=1.8,Gi=.05,Ki=1/60,qi=()=>{Hi&&cancelAnimationFrame(Hi),Hi=requestAnimationFrame(e=>{if(!z||!Li)return;let t=Ui?Math.min((e-Ui)/1e3,Gi):Ki;Ui=e;let n=Wi*t;Li.clearRect(0,0,z.width,z.height);let r=`hsl(271, 76%, 53%)`,i=zi(),{alpha:a,current:o}=Ln(()=>{switch(i.kind){case`transition`:{var e;let t=(e=i.current)!=null&&e.alpha&&i.current.alpha>0?i.current:i.transitionTo;return{alpha:t?t.alpha:0,current:t}}case`move-out`:var t,n;return{alpha:(t=(n=i.current)==null?void 0:n.alpha)==null?0:t,current:i.current};case`idle`:return{alpha:1,current:i.current}}});switch(o==null||o.rects.forEach(e=>{Li&&(Li.shadowColor=r,Li.shadowBlur=6,Li.strokeStyle=r,Li.lineWidth=2,Li.globalAlpha=a,Li.beginPath(),Li.rect(e.left,e.top,e.width,e.height),Li.stroke(),Li.shadowBlur=0,Li.beginPath(),Li.rect(e.left,e.top,e.width,e.height),Li.stroke())}),i.kind){case`move-out`:if(i.current.alpha===0){Vi({kind:`idle`,current:null}),Ui=0;return}i.current.alpha<=.01&&(i.current.alpha=0),i.current.alpha=Math.max(0,i.current.alpha-n),qi();return;case`transition`:if(i.current&&i.current.alpha>0){i.current.alpha=Math.max(0,i.current.alpha-n),qi();return}if(i.transitionTo.alpha===1){Vi({kind:`idle`,current:i.transitionTo}),Ui=0;return}i.transitionTo.alpha=Math.min(i.transitionTo.alpha+n,1),qi();case`idle`:Ui=0;return}})},Ji=null,Yi=e=>{if(z=document.createElement(`canvas`),Li=z.getContext(`2d`,{alpha:!0}),!Li)return null;let t=window.devicePixelRatio||1,{innerWidth:n,innerHeight:r}=window;z.style.width=`${n}px`,z.style.height=`${r}px`,z.width=n*t,z.height=r*t,z.style.position=`fixed`,z.style.left=`0`,z.style.top=`0`,z.style.pointerEvents=`none`,z.style.zIndex=`2147483600`,Li.scale(t,t),e.appendChild(z),Ji&&window.removeEventListener(`resize`,Ji);let i=()=>{if(!z||!Li)return;let e=window.devicePixelRatio||1,{innerWidth:t,innerHeight:n}=window;z.style.width=`${t}px`,z.style.height=`${n}px`,z.width=t*e,z.height=n*e,Li.scale(e,e),qi()};return Ji=i,window.addEventListener(`resize`,i),Ii}})),Zi,Qi,$i,ea,ta,na=r((()=>{Zi={width:550,height:350,initialHeight:400},Qi=`react-scan-widget-settings-v2`,$i=`react-scan-toolbar-state-v1`,ea=.5,ta=2147483678})),ra,ia,aa,oa,sa,ca,la=r((()=>{na(),ra=[`class`,`style`,`data-theme`,`data-mode`,`data-color-scheme`,`data-bs-theme`,`data-mui-color-scheme`],ia=e=>{if(e.classList.contains(`dark`))return`dark`;if(e.classList.contains(`light`))return`light`;for(let n of ra){var t;let r=(t=e.getAttribute(n))==null?void 0:t.toLowerCase();if(r===`dark`||r===`light`)return r}},aa=e=>{let t=e/255;return t<=.03928?t/12.92:((t+.055)/1.055)**2.4},oa=e=>{var t;let n=(t=getComputedStyle(e).backgroundColor.match(/[\d.]+/g))==null?void 0:t.map(Number);if(!(!n||n.length<3||n[3]===0))return aa(n[0])*.2126+aa(n[1])*.7152+aa(n[2])*.0722<.18?`dark`:`light`},sa=()=>{var e,t,n,r;return(e=(t=(n=(r=ia(document.documentElement))==null?document.body?ia(document.body):void 0:r)==null?document.body?oa(document.body):void 0:n)==null?oa(document.documentElement):t)==null?`light`:e},ca=e=>{let t,n=()=>{let t=sa();e.dataset.reactScanTheme=t===`dark`?`light`:`dark`},r=()=>{var e;cancelAnimationFrame((e=t)==null?0:e),t=requestAnimationFrame(n)};n();let i=new MutationObserver(r);i.observe(document.documentElement,{attributes:!0,attributeFilter:[...ra]}),document.body&&i.observe(document.body,{attributes:!0,attributeFilter:[...ra]});let a=window.matchMedia(`(prefers-color-scheme: dark)`);return a.addEventListener(`change`,r),()=>{var e;i.disconnect(),a.removeEventListener(`change`,r),cancelAnimationFrame((e=t)==null?0:e)}}})),ua,da,fa=r((()=>{la(),ua=`/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --color-red-300: oklch(80.8% 0.114 19.571); + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-950: oklch(25.8% 0.092 26.042); + --color-yellow-300: oklch(90.5% 0.182 98.111); + --color-yellow-500: oklch(79.5% 0.184 86.047); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-purple-400: oklch(71.4% 0.203 305.504); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-purple-800: oklch(43.8% 0.218 303.724); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-zinc-400: oklch(70.5% 0.015 286.067); + --color-zinc-500: oklch(55.2% 0.016 285.938); + --color-zinc-600: oklch(44.2% 0.017 285.786); + --color-zinc-700: oklch(37% 0.013 285.805); + --color-zinc-800: oklch(27.4% 0.006 286.033); + --color-zinc-900: oklch(21% 0.006 285.885); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-black: #000; + --color-white: #fff; + --spacing: 4px; + --container-md: 448px; + --text-xs: 12px; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 14px; + --text-sm--line-height: calc(1.25 / 0.875); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --tracking-wide: 0.025em; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --blur-sm: 8px; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; + font-feature-settings: normal; + font-variation-settings: normal; + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::-moz-placeholder { + opacity: 1; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::-moz-placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + -webkit-appearance: button; + -moz-appearance: button; + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .pointer-events-auto { + pointer-events: auto; + } + .pointer-events-bounding-box { + pointer-events: bounding-box; + } + .pointer-events-none { + pointer-events: none; + } + .collapse { + visibility: collapse; + } + .visible { + visibility: visible; + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .inset-x-0 { + inset-inline: calc(var(--spacing) * 0); + } + .inset-x-1 { + inset-inline: calc(var(--spacing) * 1); + } + .inset-y-0 { + inset-block: calc(var(--spacing) * 0); + } + .start { + inset-inline-start: var(--spacing); + } + .end { + inset-inline-end: var(--spacing); + } + .-top-1 { + top: calc(var(--spacing) * -1); + } + .-top-2\\.5 { + top: calc(var(--spacing) * -2.5); + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-0\\.5 { + top: calc(var(--spacing) * 0.5); + } + .top-1\\/2 { + top: calc(1 / 2 * 100%); + } + .top-2 { + top: calc(var(--spacing) * 2); + } + .top-full { + top: 100%; + } + .-right-1 { + right: calc(var(--spacing) * -1); + } + .-right-2\\.5 { + right: calc(var(--spacing) * -2.5); + } + .right-0 { + right: calc(var(--spacing) * 0); + } + .right-0\\.5 { + right: calc(var(--spacing) * 0.5); + } + .right-2 { + right: calc(var(--spacing) * 2); + } + .right-4 { + right: calc(var(--spacing) * 4); + } + .right-full { + right: 100%; + } + .bottom-0 { + bottom: calc(var(--spacing) * 0); + } + .bottom-4 { + bottom: calc(var(--spacing) * 4); + } + .bottom-full { + bottom: 100%; + } + .left-0 { + left: calc(var(--spacing) * 0); + } + .left-3 { + left: calc(var(--spacing) * 3); + } + .left-full { + left: 100%; + } + .z-10 { + z-index: 10; + } + .z-50 { + z-index: 50; + } + .z-100 { + z-index: 100; + } + .z-\\[214748365\\] { + z-index: 214748365; + } + .z-\\[214748367\\] { + z-index: 214748367; + } + .z-\\[124124124124\\] { + z-index: 124124124124; + } + .container { + width: 100%; + @media (width >= 640px) { + max-width: 640px; + } + @media (width >= 768px) { + max-width: 768px; + } + @media (width >= 1024px) { + max-width: 1024px; + } + @media (width >= 1280px) { + max-width: 1280px; + } + @media (width >= 1536px) { + max-width: 1536px; + } + } + .m-\\[2px\\] { + margin: 2px; + } + .mx-0\\.5 { + margin-inline: calc(var(--spacing) * 0.5); + } + .mt-0\\.5 { + margin-top: calc(var(--spacing) * 0.5); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } + .mt-2\\.5 { + margin-top: calc(var(--spacing) * 2.5); + } + .mr-0\\.5 { + margin-right: calc(var(--spacing) * 0.5); + } + .mr-1 { + margin-right: calc(var(--spacing) * 1); + } + .mr-1\\.5 { + margin-right: calc(var(--spacing) * 1.5); + } + .mr-2\\.5 { + margin-right: calc(var(--spacing) * 2.5); + } + .mr-16 { + margin-right: calc(var(--spacing) * 16); + } + .mr-auto { + margin-right: auto; + } + .mb-1\\.5 { + margin-bottom: calc(var(--spacing) * 1.5); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-2\\.5 { + margin-bottom: calc(var(--spacing) * 2.5); + } + .mb-3 { + margin-bottom: calc(var(--spacing) * 3); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-px { + margin-bottom: 1px; + } + .\\!ml-0 { + margin-left: calc(var(--spacing) * 0) !important; + } + .ml-1 { + margin-left: calc(var(--spacing) * 1); + } + .ml-1\\.5 { + margin-left: calc(var(--spacing) * 1.5); + } + .ml-2\\.5 { + margin-left: calc(var(--spacing) * 2.5); + } + .ml-auto { + margin-left: auto; + } + .block { + display: block; + } + .flex { + display: flex; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .aspect-square { + aspect-ratio: 1 / 1; + } + .size-5 { + width: calc(var(--spacing) * 5); + height: calc(var(--spacing) * 5); + } + .size-full { + width: 100%; + height: 100%; + } + .h-1 { + height: calc(var(--spacing) * 1); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); + } + .h-7 { + height: calc(var(--spacing) * 7); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-\\[28px\\] { + height: 28px; + } + .h-\\[48px\\] { + height: 48px; + } + .h-\\[50px\\] { + height: 50px; + } + .h-\\[150px\\] { + height: 150px; + } + .h-\\[235px\\] { + height: 235px; + } + .h-\\[calc\\(100\\%-25px\\)\\] { + height: calc(100% - 25px); + } + .h-\\[calc\\(100\\%-40px\\)\\] { + height: calc(100% - 40px); + } + .h-\\[calc\\(100\\%-48px\\)\\] { + height: calc(100% - 48px); + } + .h-\\[calc\\(100\\%-150px\\)\\] { + height: calc(100% - 150px); + } + .h-\\[calc\\(100\\%-200px\\)\\] { + height: calc(100% - 200px); + } + .h-fit { + height: -moz-fit-content; + height: fit-content; + } + .h-full { + height: 100%; + } + .h-px { + height: 1px; + } + .h-screen { + height: 100vh; + } + .max-h-0 { + max-height: calc(var(--spacing) * 0); + } + .max-h-40 { + max-height: calc(var(--spacing) * 40); + } + .min-h-\\[48px\\] { + min-height: 48px; + } + .min-h-fit { + min-height: -moz-fit-content; + min-height: fit-content; + } + .w-1 { + width: calc(var(--spacing) * 1); + } + .w-1\\/2 { + width: calc(1 / 2 * 100%); + } + .w-1\\/3 { + width: calc(1 / 3 * 100%); + } + .w-2\\/4 { + width: calc(2 / 4 * 100%); + } + .w-3 { + width: calc(var(--spacing) * 3); + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-4\\/5 { + width: calc(4 / 5 * 100%); + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); + } + .w-80 { + width: calc(var(--spacing) * 80); + } + .w-\\[20px\\] { + width: 20px; + } + .w-\\[72px\\] { + width: 72px; + } + .w-\\[90\\%\\] { + width: 90%; + } + .w-\\[calc\\(100\\%-200px\\)\\] { + width: calc(100% - 200px); + } + .w-fit { + width: -moz-fit-content; + width: fit-content; + } + .w-full { + width: 100%; + } + .w-px { + width: 1px; + } + .w-screen { + width: 100vw; + } + .max-w-md { + max-width: var(--container-md); + } + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } + .min-w-\\[200px\\] { + min-width: 200px; + } + .min-w-fit { + min-width: -moz-fit-content; + min-width: fit-content; + } + .flex-1 { + flex: 1; + } + .shrink-0 { + flex-shrink: 0; + } + .grow { + flex-grow: 1; + } + .-translate-y-1\\/2 { + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-y-0 { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .scale-75 { + --tw-scale-x: 75%; + --tw-scale-y: 75%; + --tw-scale-z: 75%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + .-rotate-90 { + rotate: calc(90deg * -1); + } + .rotate-90 { + rotate: 90deg; + } + .rotate-180 { + rotate: 180deg; + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .animate-\\[react-scan-tooltip-in_100ms_ease-out\\] { + animation: react-scan-tooltip-in 100ms ease-out; + } + .cursor-default { + cursor: default; + } + .cursor-e-resize { + cursor: e-resize; + } + .cursor-ew-resize { + cursor: ew-resize; + } + .cursor-ew-resize { + cursor: ew-resize; + } + .cursor-grab { + cursor: grab; + } + .cursor-grabbing { + cursor: grabbing; + } + .cursor-move { + cursor: move; + } + .cursor-move { + cursor: move; + } + .cursor-nesw-resize { + cursor: nesw-resize; + } + .cursor-nesw-resize { + cursor: nesw-resize; + } + .cursor-ns-resize { + cursor: ns-resize; + } + .cursor-ns-resize { + cursor: ns-resize; + } + .cursor-nwse-resize { + cursor: nwse-resize; + } + .cursor-nwse-resize { + cursor: nwse-resize; + } + .cursor-pointer { + cursor: pointer; + } + .cursor-w-resize { + cursor: w-resize; + } + .\\[touch-action\\:none\\] { + touch-action: none; + } + .resize { + resize: both; + } + .flex-col { + flex-direction: column; + } + .items-center { + align-items: center; + } + .items-end { + align-items: flex-end; + } + .items-start { + align-items: flex-start; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .justify-end { + justify-content: flex-end; + } + .justify-start { + justify-content: flex-start; + } + .gap-0\\.5 { + gap: calc(var(--spacing) * 0.5); + } + .gap-1 { + gap: calc(var(--spacing) * 1); + } + .gap-1\\.5 { + gap: calc(var(--spacing) * 1.5); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .space-y-1\\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .gap-x-0\\.5 { + -moz-column-gap: calc(var(--spacing) * 0.5); + column-gap: calc(var(--spacing) * 0.5); + } + .gap-x-1 { + -moz-column-gap: calc(var(--spacing) * 1); + column-gap: calc(var(--spacing) * 1); + } + .gap-x-1\\.5 { + -moz-column-gap: calc(var(--spacing) * 1.5); + column-gap: calc(var(--spacing) * 1.5); + } + .gap-x-2 { + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); + } + .gap-x-3 { + -moz-column-gap: calc(var(--spacing) * 3); + column-gap: calc(var(--spacing) * 3); + } + .gap-x-4 { + -moz-column-gap: calc(var(--spacing) * 4); + column-gap: calc(var(--spacing) * 4); + } + .gap-y-0\\.5 { + row-gap: calc(var(--spacing) * 0.5); + } + .gap-y-1 { + row-gap: calc(var(--spacing) * 1); + } + .gap-y-2 { + row-gap: calc(var(--spacing) * 2); + } + .gap-y-4 { + row-gap: calc(var(--spacing) * 4); + } + .divide-y { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + .divide-zinc-800 { + :where(& > :not(:last-child)) { + border-color: var(--color-zinc-800); + } + } + .self-end { + align-self: flex-end; + } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .\\!overflow-visible { + overflow: visible !important; + } + .overflow-auto { + overflow: auto; + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-auto { + overflow-x: auto; + } + .overflow-x-hidden { + overflow-x: hidden; + } + .overflow-y-auto { + overflow-y: auto; + } + .rounded { + border-radius: 4px; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-sm { + border-radius: var(--radius-sm); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .rounded-l-md { + border-top-left-radius: var(--radius-md); + border-bottom-left-radius: var(--radius-md); + } + .rounded-l-sm { + border-top-left-radius: var(--radius-sm); + border-bottom-left-radius: var(--radius-sm); + } + .rounded-r-md { + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); + } + .rounded-r-sm { + border-top-right-radius: var(--radius-sm); + border-bottom-right-radius: var(--radius-sm); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-4 { + border-style: var(--tw-border-style); + border-width: 4px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-r { + border-right-style: var(--tw-border-style); + border-right-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-l { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + .border-l-0 { + border-left-style: var(--tw-border-style); + border-left-width: 0px; + } + .border-l-1 { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + .border-none { + --tw-border-style: none; + border-style: none; + } + .\\!border-red-500 { + border-color: var(--color-red-500) !important; + } + .border-\\[\\#1e1e1e\\] { + border-color: #1e1e1e; + } + .border-\\[\\#333\\] { + border-color: #333; + } + .border-\\[\\#27272A\\] { + border-color: #27272A; + } + .border-\\[var\\(--rs-border-subtle\\)\\] { + border-color: var(--rs-border-subtle); + } + .border-transparent { + border-color: transparent; + } + .border-zinc-800 { + border-color: var(--color-zinc-800); + } + .bg-\\[\\#0A0A0A\\] { + background-color: #0A0A0A; + } + .bg-\\[\\#1D3A66\\] { + background-color: #1D3A66; + } + .bg-\\[\\#1E1E1E\\] { + background-color: #1E1E1E; + } + .bg-\\[\\#1a2a1a\\] { + background-color: #1a2a1a; + } + .bg-\\[\\#1e1e1e\\] { + background-color: #1e1e1e; + } + .bg-\\[\\#2a1515\\] { + background-color: #2a1515; + } + .bg-\\[\\#4b4b4b\\] { + background-color: #4b4b4b; + } + .bg-\\[\\#5f3f9a\\] { + background-color: #5f3f9a; + } + .bg-\\[\\#5f3f9a\\]\\/40 { + background-color: color-mix(in oklab, #5f3f9a 40%, transparent); + } + .bg-\\[\\#6a369e\\] { + background-color: #6a369e; + } + .bg-\\[\\#8e61e3\\] { + background-color: #8e61e3; + } + .bg-\\[\\#7521c8\\] { + background-color: #7521c8; + } + .bg-\\[\\#18181B\\] { + background-color: #18181B; + } + .bg-\\[\\#18181B\\]\\/50 { + background-color: color-mix(in oklab, #18181B 50%, transparent); + } + .bg-\\[\\#27272A\\] { + background-color: #27272A; + } + .bg-\\[\\#44444a\\] { + background-color: #44444a; + } + .bg-\\[\\#141414\\] { + background-color: #141414; + } + .bg-\\[\\#214379d4\\] { + background-color: #214379d4; + } + .bg-\\[\\#412162\\] { + background-color: #412162; + } + .bg-\\[\\#EFD81A\\] { + background-color: #EFD81A; + } + .bg-\\[\\#b77116\\] { + background-color: #b77116; + } + .bg-\\[\\#b94040\\] { + background-color: #b94040; + } + .bg-\\[\\#d36cff\\] { + background-color: #d36cff; + } + .bg-\\[\\#efd81a6b\\] { + background-color: #efd81a6b; + } + .bg-\\[var\\(--rs-panel-bg\\)\\] { + background-color: var(--rs-panel-bg); + } + .bg-\\[var\\(--rs-surface-active\\)\\] { + background-color: var(--rs-surface-active); + } + .bg-black { + background-color: var(--color-black); + } + .bg-black\\/40 { + background-color: color-mix(in srgb, #000 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 40%, transparent); + } + } + .bg-green-500\\/50 { + background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-green-500) 50%, transparent); + } + } + .bg-green-500\\/60 { + background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-green-500) 60%, transparent); + } + } + .bg-neutral-700 { + background-color: var(--color-neutral-700); + } + .bg-purple-500 { + background-color: var(--color-purple-500); + } + .bg-purple-500\\/90 { + background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-purple-500) 90%, transparent); + } + } + .bg-purple-800 { + background-color: var(--color-purple-800); + } + .bg-red-500 { + background-color: var(--color-red-500); + } + .bg-red-500\\/90 { + background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-500) 90%, transparent); + } + } + .bg-red-950\\/50 { + background-color: color-mix(in srgb, oklch(25.8% 0.092 26.042) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-950) 50%, transparent); + } + } + .bg-transparent { + background-color: transparent; + } + .bg-white { + background-color: var(--color-white); + } + .bg-white\\/25 { + background-color: color-mix(in srgb, #fff 25%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 25%, transparent); + } + } + .bg-yellow-300 { + background-color: var(--color-yellow-300); + } + .bg-zinc-800 { + background-color: var(--color-zinc-800); + } + .bg-zinc-900\\/30 { + background-color: color-mix(in srgb, oklch(21% 0.006 285.885) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-zinc-900) 30%, transparent); + } + } + .bg-zinc-900\\/50 { + background-color: color-mix(in srgb, oklch(21% 0.006 285.885) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-zinc-900) 50%, transparent); + } + } + .p-0 { + padding: calc(var(--spacing) * 0); + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-3 { + padding: calc(var(--spacing) * 3); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-5 { + padding: calc(var(--spacing) * 5); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .px-1 { + padding-inline: calc(var(--spacing) * 1); + } + .px-1\\.5 { + padding-inline: calc(var(--spacing) * 1.5); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-2\\.5 { + padding-inline: calc(var(--spacing) * 2.5); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .py-0\\.5 { + padding-block: calc(var(--spacing) * 0.5); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-1\\.5 { + padding-block: calc(var(--spacing) * 1.5); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .py-\\[1px\\] { + padding-block: 1px; + } + .py-\\[3px\\] { + padding-block: 3px; + } + .py-\\[5px\\] { + padding-block: 5px; + } + .pt-0 { + padding-top: calc(var(--spacing) * 0); + } + .pt-2 { + padding-top: calc(var(--spacing) * 2); + } + .pt-5 { + padding-top: calc(var(--spacing) * 5); + } + .pr-1 { + padding-right: calc(var(--spacing) * 1); + } + .pr-1\\.5 { + padding-right: calc(var(--spacing) * 1.5); + } + .pr-2 { + padding-right: calc(var(--spacing) * 2); + } + .pb-2 { + padding-bottom: calc(var(--spacing) * 2); + } + .pl-1 { + padding-left: calc(var(--spacing) * 1); + } + .pl-2 { + padding-left: calc(var(--spacing) * 2); + } + .pl-3 { + padding-left: calc(var(--spacing) * 3); + } + .pl-5 { + padding-left: calc(var(--spacing) * 5); + } + .pl-6 { + padding-left: calc(var(--spacing) * 6); + } + .text-left { + text-align: left; + } + .font-mono { + font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; + } + .font-sans { + font-family: var(--font-sans); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .text-\\[8px\\] { + font-size: 8px; + } + .text-\\[10px\\] { + font-size: 10px; + } + .text-\\[11px\\] { + font-size: 11px; + } + .text-\\[13px\\] { + font-size: 13px; + } + .text-\\[14px\\] { + font-size: 14px; + } + .text-\\[17px\\] { + font-size: 17px; + } + .leading-4 { + --tw-leading: calc(var(--spacing) * 4); + line-height: calc(var(--spacing) * 4); + } + .leading-6 { + --tw-leading: calc(var(--spacing) * 6); + line-height: calc(var(--spacing) * 6); + } + .leading-none { + --tw-leading: 1; + line-height: 1; + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-wide { + --tw-tracking: var(--tracking-wide); + letter-spacing: var(--tracking-wide); + } + .text-wrap { + text-wrap: wrap; + } + .break-words { + overflow-wrap: break-word; + } + .break-all { + word-break: break-all; + } + .whitespace-nowrap { + white-space: nowrap; + } + .whitespace-pre-wrap { + white-space: pre-wrap; + } + .text-\\[\\#4ade80\\] { + color: #4ade80; + } + .text-\\[\\#5a5a5a\\] { + color: #5a5a5a; + } + .text-\\[\\#6E6E77\\] { + color: #6E6E77; + } + .text-\\[\\#6F6F78\\] { + color: #6F6F78; + } + .text-\\[\\#8e61e3\\] { + color: #8e61e3; + } + .text-\\[\\#666\\] { + color: #666; + } + .text-\\[\\#888\\] { + color: #888; + } + .text-\\[\\#7346a0\\] { + color: #7346a0; + } + .text-\\[\\#65656D\\] { + color: #65656D; + } + .text-\\[\\#737373\\] { + color: #737373; + } + .text-\\[\\#A1A1AA\\] { + color: #A1A1AA; + } + .text-\\[\\#A855F7\\] { + color: #A855F7; + } + .text-\\[\\#E4E4E7\\] { + color: #E4E4E7; + } + .text-\\[\\#d36cff\\] { + color: #d36cff; + } + .text-\\[\\#f87171\\] { + color: #f87171; + } + .text-\\[var\\(--rs-text-primary\\)\\] { + color: var(--rs-text-primary); + } + .text-\\[var\\(--rs-text-secondary\\)\\] { + color: var(--rs-text-secondary); + } + .text-black { + color: var(--color-black); + } + .text-current { + color: currentcolor; + } + .text-gray-100 { + color: var(--color-gray-100); + } + .text-gray-400 { + color: var(--color-gray-400); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-green-500 { + color: var(--color-green-500); + } + .text-neutral-300 { + color: var(--color-neutral-300); + } + .text-neutral-400 { + color: var(--color-neutral-400); + } + .text-neutral-500 { + color: var(--color-neutral-500); + } + .text-purple-400 { + color: var(--color-purple-400); + } + .text-red-300 { + color: var(--color-red-300); + } + .text-red-400 { + color: var(--color-red-400); + } + .text-red-500 { + color: var(--color-red-500); + } + .text-white { + color: var(--color-white); + } + .text-white\\/30 { + color: color-mix(in srgb, #fff 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 30%, transparent); + } + } + .text-white\\/70 { + color: color-mix(in srgb, #fff 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 70%, transparent); + } + } + .text-yellow-300 { + color: var(--color-yellow-300); + } + .text-yellow-500 { + color: var(--color-yellow-500); + } + .text-zinc-400 { + color: var(--color-zinc-400); + } + .text-zinc-500 { + color: var(--color-zinc-500); + } + .text-zinc-600 { + color: var(--color-zinc-600); + } + .uppercase { + text-transform: uppercase; + } + .italic { + font-style: italic; + } + .opacity-0 { + opacity: 0%; + } + .opacity-50 { + opacity: 50%; + } + .opacity-80 { + opacity: 80%; + } + .opacity-100 { + opacity: 100%; + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring-1 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .\\[box-shadow\\:var\\(--rs-shadow\\)\\] { + box-shadow: var(--rs-shadow); + } + .ring-white\\/\\[0\\.08\\] { + --tw-ring-color: color-mix(in srgb, #fff 8%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-white) 8%, transparent); + } + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur-sm { + --tw-backdrop-blur: blur(var(--blur-sm)); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-\\[color\\,transform\\] { + transition-property: color,transform; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-\\[max-height\\] { + transition-property: max-height; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-\\[opacity\\] { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-opacity { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-none { + transition-property: none; + } + .delay-0 { + transition-delay: 0ms; + } + .delay-150 { + transition-delay: 150ms; + } + .delay-300 { + transition-delay: 300ms; + } + .\\!duration-0 { + --tw-duration: 0ms !important; + transition-duration: 0ms !important; + } + .duration-0 { + --tw-duration: 0ms; + transition-duration: 0ms; + } + .duration-120 { + --tw-duration: 120ms; + transition-duration: 120ms; + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .ease-\\[cubic-bezier\\(0\\.25\\,0\\.1\\,0\\.25\\,1\\)\\] { + --tw-ease: cubic-bezier(0.25,0.1,0.25,1); + transition-timing-function: cubic-bezier(0.25,0.1,0.25,1); + } + .ease-in { + --tw-ease: var(--ease-in); + transition-timing-function: var(--ease-in); + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } + .\\[will-change\\:transform\\] { + will-change: transform; + } + .will-change-transform { + will-change: transform; + } + .select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } + .animation-delay-0 { + animation-delay: 0s; + } + .animation-delay-100 { + animation-delay: .1s; + } + .animation-delay-150 { + animation-delay: .15s; + } + .animation-delay-200 { + animation-delay: .2s; + } + .animation-delay-300 { + animation-delay: .3s; + } + .animation-delay-500 { + animation-delay: .5s; + } + .animation-delay-700 { + animation-delay: .7s; + } + .animation-delay-1000 { + animation-delay: 1s; + } + .animation-duration-0 { + animation-duration: 0s; + } + .animation-duration-100 { + animation-duration: .1s; + } + .animation-duration-200 { + animation-duration: .2s; + } + .animation-duration-300 { + animation-duration: .3s; + } + .animation-duration-500 { + animation-duration: .5s; + } + .animation-duration-700 { + animation-duration: .7s; + } + .animation-duration-1000 { + animation-duration: 1s; + } + .group-hover\\:bg-\\[\\#5b2d89\\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #5b2d89; + } + } + } + .group-hover\\:bg-\\[\\#6a6a6a\\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #6a6a6a; + } + } + } + .group-hover\\:bg-\\[\\#21437982\\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #21437982; + } + } + } + .group-hover\\:bg-\\[\\#efda1a2f\\] { + &:is(:where(.group):hover *) { + @media (hover: hover) { + background-color: #efda1a2f; + } + } + } + .group-hover\\:opacity-100 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + opacity: 100%; + } + } + } + .peer-hover\\/bottom\\:rounded-b-none { + &:is(:where(.peer\\/bottom):hover ~ *) { + @media (hover: hover) { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + } + } + .peer-hover\\/left\\:rounded-l-none { + &:is(:where(.peer\\/left):hover ~ *) { + @media (hover: hover) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + } + .peer-hover\\/right\\:rounded-r-none { + &:is(:where(.peer\\/right):hover ~ *) { + @media (hover: hover) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } + } + .peer-hover\\/top\\:rounded-t-none { + &:is(:where(.peer\\/top):hover ~ *) { + @media (hover: hover) { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + } + .after\\:absolute { + &::after { + content: var(--tw-content); + position: absolute; + } + } + .after\\:inset-0 { + &::after { + content: var(--tw-content); + inset: calc(var(--spacing) * 0); + } + } + .after\\:top-\\[100\\%\\] { + &::after { + content: var(--tw-content); + top: 100%; + } + } + .after\\:left-1\\/2 { + &::after { + content: var(--tw-content); + left: calc(1 / 2 * 100%); + } + } + .after\\:h-\\[6px\\] { + &::after { + content: var(--tw-content); + height: 6px; + } + } + .after\\:w-\\[10px\\] { + &::after { + content: var(--tw-content); + width: 10px; + } + } + .after\\:-translate-x-1\\/2 { + &::after { + content: var(--tw-content); + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .after\\:animate-\\[fadeOut_1s_ease-out_forwards\\] { + &::after { + content: var(--tw-content); + animation: fadeOut 1s ease-out forwards; + } + } + .after\\:border-t-\\[6px\\] { + &::after { + content: var(--tw-content); + border-top-style: var(--tw-border-style); + border-top-width: 6px; + } + } + .after\\:border-r-\\[5px\\] { + &::after { + content: var(--tw-content); + border-right-style: var(--tw-border-style); + border-right-width: 5px; + } + } + .after\\:border-l-\\[5px\\] { + &::after { + content: var(--tw-content); + border-left-style: var(--tw-border-style); + border-left-width: 5px; + } + } + .after\\:border-t-white { + &::after { + content: var(--tw-content); + border-top-color: var(--color-white); + } + } + .after\\:border-r-transparent { + &::after { + content: var(--tw-content); + border-right-color: transparent; + } + } + .after\\:border-l-transparent { + &::after { + content: var(--tw-content); + border-left-color: transparent; + } + } + .after\\:bg-purple-500\\/30 { + &::after { + content: var(--tw-content); + background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-purple-500) 30%, transparent); + } + } + } + .after\\:content-\\[\\"\\"\\] { + &::after { + --tw-content: ""; + content: var(--tw-content); + } + } + .focus-within\\:border-\\[\\#454545\\] { + &:focus-within { + border-color: #454545; + } + } + .hover\\:bg-\\[\\#0f0f0f\\] { + &:hover { + @media (hover: hover) { + background-color: #0f0f0f; + } + } + } + .hover\\:bg-\\[\\#5f3f9a\\]\\/20 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #5f3f9a 20%, transparent); + } + } + } + .hover\\:bg-\\[\\#5f3f9a\\]\\/40 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #5f3f9a 40%, transparent); + } + } + } + .hover\\:bg-\\[\\#18181B\\] { + &:hover { + @media (hover: hover) { + background-color: #18181B; + } + } + } + .hover\\:bg-\\[\\#34343b\\] { + &:hover { + @media (hover: hover) { + background-color: #34343b; + } + } + } + .hover\\:bg-\\[var\\(--rs-surface-hover\\)\\] { + &:hover { + @media (hover: hover) { + background-color: var(--rs-surface-hover); + } + } + } + .hover\\:bg-red-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-600); + } + } + } + .hover\\:bg-zinc-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-zinc-700); + } + } + } + .hover\\:bg-zinc-800\\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(27.4% 0.006 286.033) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-zinc-800) 50%, transparent); + } + } + } + } + .hover\\:text-neutral-300 { + &:hover { + @media (hover: hover) { + color: var(--color-neutral-300); + } + } + } +} +:host, +:host([data-react-scan-theme="dark"]) { + --rs-panel-bg: #161616; + --rs-panel-raised: #202020; + --rs-text-primary: #fff; + --rs-text-secondary: #a7a7a7; + --rs-surface-hover: rgb(255 255 255 / 10%); + --rs-surface-active: rgb(255 255 255 / 15%); + --rs-border-subtle: rgb(255 255 255 / 10%); + --rs-shadow: 0 2px 8px rgb(0 0 0 / 8%); + --rs-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); + --rs-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); +} +:host([data-react-scan-theme="light"]) { + --rs-panel-bg: #fff; + --rs-panel-raised: #f5f5f5; + --rs-text-primary: #171717; + --rs-text-secondary: #737373; + --rs-surface-hover: rgb(0 0 0 / 5%); + --rs-surface-active: rgb(0 0 0 / 8%); + --rs-border-subtle: rgb(0 0 0 / 8%); + --rs-shadow: 0 2px 12px rgb(0 0 0 / 10%); +} +@keyframes react-scan-tooltip-in { + from { + opacity: 0; + transform: scale(0.97); + } + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes react-scan-shake { + 0%, + 100% { + translate: 0; + } + 25% { + translate: -3px; + } + 50% { + translate: 3px; + } + 75% { + translate: -2px; + } +} +.react-scan-interactive-scale { + transition: transform 400ms var(--rs-ease-spring); + @media (hover: hover) and (pointer: fine) { + &:hover { + transform: scale(1.05); + } + } + &:active { + transform: scale(0.96); + transition: transform 60ms cubic-bezier(0, 0, 0.2, 1); + } +} +.react-scan-a11y-hitbox { + position: relative; + &::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: max(100%, 24px); + height: max(100%, 24px); + transform: translate(-50%, -50%); + pointer-events: auto; + } +} +@media (prefers-reduced-motion: reduce) { + :host, + :host *, + :host *::before, + :host *::after { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +} +* { + outline: none !important; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + &::-webkit-scrollbar-track { + border-radius: 10px; + background: transparent; + } + &::-webkit-scrollbar-thumb { + border-radius: 10px; + background: rgba(255, 255, 255, 0.3); + } + &::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.4); + } + &::-webkit-scrollbar-corner { + background: transparent; + } +} +@-moz-document url-prefix() { + * { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.4) transparent; + scrollbar-width: 6px; + } +} +button { + &:hover { + @media (hover: hover) { + background-image: none; + } + } + --tw-outline-style: none; + outline-style: none; + --tw-border-style: none; + border-style: none; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + cursor: pointer; +} +input { + --tw-outline-style: none; + outline-style: none; + --tw-border-style: none; + border-style: none; + background-color: transparent; + background-image: none; + &::-moz-placeholder { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + &::placeholder { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + &::-moz-placeholder { + color: var(--color-neutral-500); + } + &::placeholder { + color: var(--color-neutral-500); + } + &::-moz-placeholder { + font-style: italic; + } + &::placeholder { + font-style: italic; + } + &:-moz-placeholder { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + &:placeholder-shown { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} +svg { + height: auto; + width: auto; + pointer-events: none; +} +.with-data-text { + overflow: hidden; + &::before { + content: attr(data-text); + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} +#react-scan-toolbar { + position: fixed; + top: calc(var(--spacing) * 0); + left: calc(var(--spacing) * 0); + display: flex; + flex-direction: column; + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; + font-size: 13px; + color: var(--color-white); + background-color: var(--color-black); + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + cursor: move; + opacity: 0%; + z-index: 2147483678; + animation: fadeIn ease-in forwards; + animation-delay: .3s; + animation-duration: .3s; + --tw-shadow: 0 4px 12px var(--tw-shadow-color, rgba(0,0,0,0.2)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + place-self: start; + will-change: transform; + backface-visibility: hidden; +} +#react-scan-toolbar pre, +#react-scan-toolbar textarea, +#react-scan-toolbar input[type="text"], +#react-scan-toolbar input[type="search"], +#react-scan-toolbar [data-react-scan-selectable] { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + cursor: text; +} +.button { + &:hover { + background: rgba(255, 255, 255, 0.1); + } + &:active { + background: rgba(255, 255, 255, 0.15); + } +} +.resize-line-wrapper { + position: absolute; + overflow: hidden; +} +.resize-line { + position: absolute; + inset: calc(var(--spacing) * 0); + overflow: hidden; + background-color: var(--color-black); + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + svg { + position: absolute; + top: calc(1 / 2 * 100%); + left: calc(1 / 2 * 100%); + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.resize-right, +.resize-left { + inset-block: calc(var(--spacing) * 0); + width: calc(var(--spacing) * 6); + cursor: ew-resize; + .resize-line-wrapper { + inset-block: calc(var(--spacing) * 0); + width: calc(1 / 2 * 100%); + } + &:hover { + .resize-line { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } +} +.resize-right { + right: calc(var(--spacing) * 0); + --tw-translate-x: calc(1 / 2 * 100%); + translate: var(--tw-translate-x) var(--tw-translate-y); + .resize-line-wrapper { + right: calc(var(--spacing) * 0); + } + .resize-line { + border-top-right-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.resize-left { + left: calc(var(--spacing) * 0); + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + .resize-line-wrapper { + left: calc(var(--spacing) * 0); + } + .resize-line { + border-top-left-radius: var(--radius-lg); + border-bottom-left-radius: var(--radius-lg); + --tw-translate-x: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.resize-top, +.resize-bottom { + inset-inline: calc(var(--spacing) * 0); + height: calc(var(--spacing) * 6); + cursor: ns-resize; + .resize-line-wrapper { + inset-inline: calc(var(--spacing) * 0); + height: calc(1 / 2 * 100%); + } + &:hover { + .resize-line { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } +} +.resize-top { + top: calc(var(--spacing) * 0); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + .resize-line-wrapper { + top: calc(var(--spacing) * 0); + } + .resize-line { + border-top-left-radius: var(--radius-lg); + border-top-right-radius: var(--radius-lg); + --tw-translate-y: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.resize-bottom { + bottom: calc(var(--spacing) * 0); + --tw-translate-y: calc(1 / 2 * 100%); + translate: var(--tw-translate-x) var(--tw-translate-y); + .resize-line-wrapper { + bottom: calc(var(--spacing) * 0); + } + .resize-line { + border-bottom-right-radius: var(--radius-lg); + border-bottom-left-radius: var(--radius-lg); + --tw-translate-y: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.react-scan-header { + display: flex; + align-items: center; + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); + padding-right: calc(var(--spacing) * 2); + padding-left: calc(var(--spacing) * 3); + min-height: calc(var(--spacing) * 9); + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + border-color: #222; + overflow: hidden; + white-space: nowrap; +} +.react-scan-replay-button, +.react-scan-close-button { + display: flex; + align-items: center; + padding: calc(var(--spacing) * 1); + min-width: -moz-fit-content; + min-width: fit-content; + border-radius: 4px; + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; +} +.react-scan-replay-button { + position: relative; + overflow: hidden; + background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 50%, transparent) !important; + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-purple-500) 50%, transparent) !important; + } + &:hover { + background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 25%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-purple-500) 25%, transparent); + } + } + &.disabled { + opacity: 50%; + pointer-events: none; + } + &:before { + content: ""; + position: absolute; + inset: calc(var(--spacing) * 0); + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + animation: shimmer 2s infinite; + background: linear-gradient(to right, transparent, rgba(142, 97, 227, 0.3), transparent); + } +} +.react-scan-close-button { + background-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } + &:hover { + background-color: color-mix(in srgb, #fff 15%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 15%, transparent); + } + } +} +@keyframes shimmer { + 100% { + transform: translateX(100%); + } +} +.react-section-header { + position: sticky; + z-index: 100; + display: flex; + align-items: center; + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); + padding-inline: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 7); + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #888; + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + border-color: #222; + background-color: #0a0a0a; +} +.react-scan-section { + display: flex; + flex-direction: column; + padding-inline: calc(var(--spacing) * 2); + color: #888; + &::before { + content: var(--tw-content); + color: var(--color-gray-500); + } + &::before { + --tw-content: attr(data-section); + content: var(--tw-content); + } + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + > .react-scan-property { + margin-left: calc(14px * -1); + } +} +.react-scan-property { + position: relative; + display: flex; + flex-direction: column; + padding-left: calc(var(--spacing) * 8); + border-left-style: var(--tw-border-style); + border-left-width: 1px; + border-color: transparent; + overflow: hidden; +} +.react-scan-property-content { + display: flex; + flex: 1; + flex-direction: column; + min-height: calc(var(--spacing) * 7); + max-width: 100%; + overflow: hidden; +} +.react-scan-string { + color: #9ecbff; +} +.react-scan-number { + color: #79c7ff; +} +.react-scan-boolean { + color: #56b6c2; +} +.react-scan-key { + width: -moz-fit-content; + width: fit-content; + max-width: calc(var(--spacing) * 60); + white-space: nowrap; + color: var(--color-white); +} +.react-scan-input { + color: var(--color-white); + background-color: var(--color-black); +} +@keyframes blink { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.react-scan-arrow { + position: absolute; + top: calc(var(--spacing) * 0); + left: calc(var(--spacing) * 7); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + height: calc(var(--spacing) * 7); + width: calc(var(--spacing) * 6); + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + z-index: 10; + > svg { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } +} +.react-scan-nested { + position: relative; + overflow: hidden; + &:before { + content: ""; + position: absolute; + top: calc(var(--spacing) * 0); + left: calc(var(--spacing) * 0); + height: 100%; + width: 1px; + background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-gray-500) 30%, transparent); + } + } +} +.react-scan-settings { + position: absolute; + inset: calc(var(--spacing) * 0); + display: flex; + flex-direction: column; + gap: calc(var(--spacing) * 4); + padding-inline: calc(var(--spacing) * 4); + padding-block: calc(var(--spacing) * 2); + color: #888; + > div { + display: flex; + align-items: center; + justify-content: space-between; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; + } +} +.react-scan-preview-line { + position: relative; + display: flex; + min-height: calc(var(--spacing) * 7); + align-items: center; + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); +} +.react-scan-flash-overlay { + position: absolute; + inset: calc(var(--spacing) * 0); + opacity: 0%; + z-index: 50; + pointer-events: none; + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + mix-blend-mode: multiply; + background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-purple-500) 90%, transparent); + } +} +.react-scan-toggle { + position: relative; + display: inline-flex; + height: calc(var(--spacing) * 6); + width: calc(var(--spacing) * 10); + input { + position: absolute; + inset: calc(var(--spacing) * 0); + z-index: 20; + opacity: 0%; + cursor: pointer; + height: 100%; + width: 100%; + } + input:checked { + + div { + background-color: #5f3f9a; + &::before { + --tw-translate-x: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + left: auto; + border-color: #5f3f9a; + } + } + } + > div { + position: absolute; + inset: calc(var(--spacing) * 1); + background-color: var(--color-neutral-700); + border-radius: calc(infinity * 1px); + pointer-events: none; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; + &:before { + --tw-content: ''; + content: var(--tw-content); + position: absolute; + top: calc(1 / 2 * 100%); + left: calc(var(--spacing) * 0); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + height: calc(var(--spacing) * 4); + width: calc(var(--spacing) * 4); + background-color: var(--color-white); + border-style: var(--tw-border-style); + border-width: 2px; + border-color: var(--color-neutral-700); + border-radius: calc(infinity * 1px); + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; + } + } +} +.react-scan-flash-active { + opacity: 40%; + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; +} +.react-scan-inspector-overlay { + display: flex; + flex-direction: column; + opacity: 0%; + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 200ms; + transition-duration: 200ms; + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + will-change: opacity; + &.fade-out { + opacity: 0%; + } + &.fade-in { + opacity: 100%; + } +} +.react-scan-what-changed { + ul { + list-style-type: disc; + padding-left: calc(var(--spacing) * 4); + } + li { + white-space: nowrap; + > div { + display: flex; + align-items: center; + justify-content: space-between; + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); + } + } +} +.count-badge { + display: flex; + align-items: center; + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); + padding-inline: calc(var(--spacing) * 1.5); + padding-block: calc(var(--spacing) * 0.5); + border-radius: 4px; + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: #a855f7; + --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); + background-color: color-mix(in oklab, #a855f7 10%, transparent); + transform-origin: center; + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + transition-delay: 150ms; + --tw-duration: 300ms; + transition-duration: 300ms; +} +.count-flash { + animation: countFlash .3s ease-out forwards; +} +.count-flash-white { + animation: countFlashShake .3s ease-out forwards; + transition-delay: 500ms !important; +} +.change-scope { + display: flex; + align-items: center; + -moz-column-gap: calc(var(--spacing) * 1); + column-gap: calc(var(--spacing) * 1); + color: #666; + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; + > div { + padding-inline: calc(var(--spacing) * 1.5); + padding-block: calc(var(--spacing) * 0.5); + border-radius: 4px; + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); + transform-origin: center; + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + transition-delay: 150ms; + --tw-duration: 300ms; + transition-duration: 300ms; + &[data-flash="true"] { + background-color: color-mix(in oklab, #a855f7 10%, transparent); + color: #a855f7; + } + } +} +.react-scan-slider { + position: relative; + min-height: calc(var(--spacing) * 6); + > input { + position: absolute; + inset: calc(var(--spacing) * 0); + opacity: 0%; + } + &:before { + --tw-content: ''; + content: var(--tw-content); + position: absolute; + inset-inline: calc(var(--spacing) * 0); + top: calc(1 / 2 * 100%); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + height: calc(var(--spacing) * 1.5); + background-color: color-mix(in oklab, #8e61e3 40%, transparent); + border-radius: var(--radius-lg); + pointer-events: none; + } + &:after { + --tw-content: ''; + content: var(--tw-content); + position: absolute; + inset-inline: calc(var(--spacing) * 0); + inset-block: calc(var(--spacing) * -2); + z-index: calc(10 * -1); + } + span { + position: absolute; + top: calc(1 / 2 * 100%); + left: calc(var(--spacing) * 0); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + height: calc(var(--spacing) * 2.5); + width: calc(var(--spacing) * 2.5); + border-radius: var(--radius-lg); + background-color: #8e61e3; + pointer-events: none; + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 75ms; + transition-duration: 75ms; + } +} +.resize-v-line { + display: flex; + align-items: center; + justify-content: center; + max-width: calc(var(--spacing) * 1); + min-width: calc(var(--spacing) * 1); + height: 100%; + width: 100%; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:hover, + &:active { + > span { + background-color: #222; + } + svg { + opacity: 100%; + } + } + &::before { + --tw-content: ""; + content: var(--tw-content); + position: absolute; + inset: calc(var(--spacing) * 0); + left: calc(1 / 2 * 100%); + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + width: 1px; + background-color: #222; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + > span { + position: absolute; + top: calc(1 / 2 * 100%); + left: calc(1 / 2 * 100%); + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + height: 18px; + width: calc(var(--spacing) * 1.5); + border-radius: 4px; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + svg { + position: absolute; + top: calc(1 / 2 * 100%); + left: calc(1 / 2 * 100%); + --tw-translate-x: calc(calc(1 / 2 * 100%) * -1); + --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + rotate: 90deg; + color: var(--color-neutral-400); + opacity: 0%; + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + z-index: 50; + } +} +.tree-node-search-highlight { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + span { + padding-block: 1px; + border-radius: var(--radius-sm); + background-color: var(--color-yellow-300); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-black); + } + .single { + margin-right: 1px; + padding-inline: 2px; + } + .regex { + padding-inline: 2px; + } + .start { + margin-left: 1px; + border-top-left-radius: var(--radius-sm); + border-bottom-left-radius: var(--radius-sm); + } + .end { + margin-right: 1px; + border-top-right-radius: var(--radius-sm); + border-bottom-right-radius: var(--radius-sm); + } + .middle { + margin-inline: 1px; + border-radius: var(--radius-sm); + } +} +.react-scan-toolbar-notification { + position: absolute; + inset-inline: calc(var(--spacing) * 0); + display: flex; + align-items: center; + -moz-column-gap: calc(var(--spacing) * 2); + column-gap: calc(var(--spacing) * 2); + padding: calc(var(--spacing) * 1); + padding-left: calc(var(--spacing) * 2); + font-size: 10px; + color: var(--color-neutral-300); + background-color: color-mix(in srgb, #000 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 90%, transparent); + } + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:before { + --tw-content: ''; + content: var(--tw-content); + position: absolute; + inset-inline: calc(var(--spacing) * 0); + background-color: var(--color-black); + height: calc(var(--spacing) * 2); + } + &.position-top { + top: 100%; + --tw-translate-y: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + border-bottom-right-radius: var(--radius-lg); + border-bottom-left-radius: var(--radius-lg); + &::before { + top: calc(var(--spacing) * 0); + --tw-translate-y: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + &.position-bottom { + bottom: 100%; + --tw-translate-y: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + border-top-left-radius: var(--radius-lg); + border-top-right-radius: var(--radius-lg); + &::before { + bottom: calc(var(--spacing) * 0); + --tw-translate-y: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + &.is-open { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.react-scan-header-item { + position: absolute; + inset: calc(var(--spacing) * 0); + --tw-translate-y: calc(200% * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 300ms; + transition-duration: 300ms; + &.is-visible { + --tw-translate-y: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } +} +.react-scan-components-tree:has(.resize-v-line:hover, .resize-v-line:active) .tree { + overflow: hidden; +} +.react-scan-expandable { + display: grid; + grid-template-rows: 0fr; + overflow: hidden; + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + --tw-duration: 75ms; + transition-duration: 75ms; + transition-timing-function: ease-out; + > * { + min-height: 0; + } + &.react-scan-expanded { + grid-template-rows: 1fr; + transition-duration: 100ms; + } +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-scale-x { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-y { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-z { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} +@property --tw-ordinal { + syntax: "*"; + inherits: false; +} +@property --tw-slashed-zero { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-figure { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-spacing { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-fraction { + syntax: "*"; + inherits: false; +} +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +@keyframes countFlash { + 0% { + background-color: rgba(168, 85, 247, 0.3); + transform: scale(1.05); + } + 100% { + background-color: rgba(168, 85, 247, 0.1); + transform: scale(1); + } +} +@keyframes countFlashShake { + 0% { + transform: translateX(0); + } + 25% { + transform: translateX(-5px); + } + 50% { + transform: translateX(5px) scale(1.1); + } + 75% { + transform: translateX(-5px); + } + 100% { + transform: translateX(0); + } +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-scale-z: 1; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-divide-y-reverse: 0; + --tw-border-style: solid; + --tw-leading: initial; + --tw-font-weight: initial; + --tw-tracking: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-duration: initial; + --tw-ease: initial; + --tw-content: ""; + --tw-ordinal: initial; + --tw-slashed-zero: initial; + --tw-numeric-figure: initial; + --tw-numeric-spacing: initial; + --tw-numeric-fraction: initial; + } + } +} +`,da=()=>{let e=document.createElement(`div`);e.id=`react-scan-root`;let t=e.attachShadow({mode:`open`}),n=document.createElement(`style`);n.textContent=ua,t.appendChild(n),document.documentElement.appendChild(e);let r=ca(e);return{shadowRoot:t,dispose:()=>{r(),e.remove()}}}}));function pa(e,t){let n=Ba[e];return typeof n==`object`?n[t]?n.$:void 0:n}function ma(e,t,n){let r=n.length,i=t.length,a=r,o=0,s=0,c=t[i-1].nextSibling,l=null;for(;or-s){let i=t[o];for(;s{i=r,t===document?e():U(t,e(),t.firstChild?null:void 0,n)},r.owner),()=>{i(),t.textContent=``}}function B(e,t,n,r){let i,a=()=>{let t=r?document.createElementNS(`http://www.w3.org/1998/Math/MathML`,`template`):document.createElement(`template`);return t.innerHTML=e,n?t.content.firstChild.firstChild:r?t.firstChild:t.content.firstChild},o=t?()=>$e(()=>document.importNode(i||(i=a()),!0)):()=>(i||(i=a())).cloneNode(!0);return o.cloneNode=o,o}function ga(e,t=window.document){let n=t[Ua]||(t[Ua]=new Set);for(let r=0,i=e.length;rr.call(e,n[1],t))}else e.addEventListener(t,n,typeof n!=`function`&&n)}function ba(e,t,n={}){let r=Object.keys(t||{}),i=Object.keys(n),a,o;for(a=0,o=i.length;ai.children=ja(e,t.children,i.children)),E(()=>typeof t.ref==`function`&&wa(t.ref,e)),E(()=>Ta(e,t,n,!0,i,!0)),i}function wa(e,t,n){return $e(()=>e(t,n))}function U(e,t,n,r){if(n!==void 0&&!r&&(r=[]),typeof t!=`function`)return ja(e,t,r,n);E(r=>ja(e,t(),r,n),r)}function Ta(e,t,n,r,i={},a=!1){t||(t={});for(let r in i)if(!(r in t)){if(r===`children`)continue;i[r]=ka(e,r,null,i[r],n,a,t)}for(let o in t){if(o===`children`){r||ja(e,t.children);continue}let s=t[o];i[o]=ka(e,o,s,i[o],n,a,t)}}function Ea(e){return!!M.context&&!M.done&&(!e||e.isConnected)}function Da(e){return e.toLowerCase().replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function Oa(e,t,n){let r=t.trim().split(/\s+/);for(let t=0,i=r.length;t-1&&Ha[t.split(`:`)[0]];r?_a(e,r,t,n):V(e,za[t]||t,n)}return n}function Aa(e){if(M.registry&&M.events&&M.events.find(([t,n])=>n===e))return;let t=e.target,n=`$$${e.type}`,r=e.target,i=e.currentTarget,a=t=>Object.defineProperty(e,`target`,{configurable:!0,value:t}),o=()=>{let r=t[n];if(r&&!t.disabled){let i=t[`${n}Data`];if(i===void 0?r.call(t,e):r.call(t,i,e),e.cancelBubble)return}return t.host&&typeof t.host!=`string`&&!t.host._$host&&t.contains(e.target)&&a(t.host),!0},s=()=>{for(;o()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,`currentTarget`,{configurable:!0,get(){return t||document}}),M.registry&&!M.done&&(M.done=_$HY.done=!0),e.composedPath){let n=e.composedPath();a(n[0]);for(let e=0;e{let i=t();for(;typeof i==`function`;)i=i();n=ja(e,i,n,r)}),()=>n;else if(Array.isArray(t)){let o=[],c=n&&Array.isArray(n);if(Ma(o,t,n,i))return E(()=>n=ja(e,o,n,r,!0)),()=>n;if(a){if(!o.length)return n;if(r===void 0)return n=[...e.childNodes];let t=o[0];if(t.parentNode!==e)return n;let i=[t];for(;(t=t.nextSibling)!==r;)i.push(t);return n=i}if(o.length===0){if(n=Pa(e,n,r),s)return n}else c?n.length===0?Na(e,o,r):ma(e,n,o):(n&&Pa(e),Na(e,o));n=o}else if(t.nodeType){if(a&&t.parentNode)return n=s?[t]:t;if(Array.isArray(n)){if(s)return n=Pa(e,n,r,t);Pa(e,n,null,t)}else n==null||n===``||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}return n}function Ma(e,t,n,r){let i=!1;for(let a=0,o=t.length;a=0;a--){let o=t[a];if(i!==o){let t=o.parentNode===e;!r&&!a?t?e.replaceChild(i,o):e.insertBefore(i,n):t&&o.remove()}else r=!0}}else e.insertBefore(i,n);return[i]}function Fa(e,t=!1,n=void 0){return t?document.createElementNS(Wa,e):document.createElement(e,{is:n})}function Ia(e){let{useShadow:t}=e,n=document.createTextNode(``),r=()=>e.mount||document.body,i=it(),a,o=!!M.context;return D(()=>{o&&(it().user=o=!1),a||(a=at(i,()=>O(()=>e.children)));let s=r();if(s instanceof HTMLHeadElement){let[e,t]=T(!1);Ze(t=>U(s,()=>e()?t():a(),null)),k(()=>t(!0))}else{let r=Fa(e.isSVG?`g`:`div`,e.isSVG),i=t&&r.attachShadow?r.attachShadow({mode:`open`}):r;Object.defineProperty(r,`_$host`,{get(){return n.parentNode},configurable:!0}),U(i,a),s.appendChild(r),e.ref&&e.ref(r),k(()=>s.contains(r)&&s.removeChild(r))}},void 0,{render:!o}),n}var La,Ra,za,Ba,Va,Ha,W,Ua,Wa,G=r((()=>{I(),La=new Set([`className`,`value`,`readOnly`,`noValidate`,`formNoValidate`,`isMap`,`noModule`,`playsInline`,`adAuctionHeaders`,`allowFullscreen`,`browsingTopics`,`defaultChecked`,`defaultMuted`,`defaultSelected`,`disablePictureInPicture`,`disableRemotePlayback`,`preservesPitch`,`shadowRootClonable`,`shadowRootCustomElementRegistry`,`shadowRootDelegatesFocus`,`shadowRootSerializable`,`sharedStorageWritable`,...`allowfullscreen.async.alpha.autofocus.autoplay.checked.controls.default.disabled.formnovalidate.hidden.indeterminate.inert.ismap.loop.multiple.muted.nomodule.novalidate.open.playsinline.readonly.required.reversed.seamless.selected.adauctionheaders.browsingtopics.credentialless.defaultchecked.defaultmuted.defaultselected.defer.disablepictureinpicture.disableremoteplayback.preservespitch.shadowrootclonable.shadowrootcustomelementregistry.shadowrootdelegatesfocus.shadowrootserializable.sharedstoragewritable`.split(`.`)]),Ra=new Set([`innerHTML`,`textContent`,`innerText`,`children`]),za=Object.assign(Object.create(null),{className:`class`,htmlFor:`for`}),Ba=Object.assign(Object.create(null),{class:`className`,novalidate:{$:`noValidate`,FORM:1},formnovalidate:{$:`formNoValidate`,BUTTON:1,INPUT:1},ismap:{$:`isMap`,IMG:1},nomodule:{$:`noModule`,SCRIPT:1},playsinline:{$:`playsInline`,VIDEO:1},readonly:{$:`readOnly`,INPUT:1,TEXTAREA:1},adauctionheaders:{$:`adAuctionHeaders`,IFRAME:1},allowfullscreen:{$:`allowFullscreen`,IFRAME:1},browsingtopics:{$:`browsingTopics`,IMG:1},defaultchecked:{$:`defaultChecked`,INPUT:1},defaultmuted:{$:`defaultMuted`,AUDIO:1,VIDEO:1},defaultselected:{$:`defaultSelected`,OPTION:1},disablepictureinpicture:{$:`disablePictureInPicture`,VIDEO:1},disableremoteplayback:{$:`disableRemotePlayback`,AUDIO:1,VIDEO:1},preservespitch:{$:`preservesPitch`,AUDIO:1,VIDEO:1},shadowrootclonable:{$:`shadowRootClonable`,TEMPLATE:1},shadowrootdelegatesfocus:{$:`shadowRootDelegatesFocus`,TEMPLATE:1},shadowrootserializable:{$:`shadowRootSerializable`,TEMPLATE:1},sharedstoragewritable:{$:`sharedStorageWritable`,IFRAME:1,IMG:1}}),Va=new Set([`beforeinput`,`click`,`dblclick`,`contextmenu`,`focusin`,`focusout`,`input`,`keydown`,`keyup`,`mousedown`,`mousemove`,`mouseout`,`mouseover`,`mouseup`,`pointerdown`,`pointermove`,`pointerout`,`pointerover`,`pointerup`,`touchend`,`touchmove`,`touchstart`]),Ha={xlink:`http://www.w3.org/1999/xlink`,xml:`http://www.w3.org/XML/1998/namespace`},W=e=>O(()=>e()),Ua=`_$DX_DELEGATE`,Wa=`http://www.w3.org/2000/svg`})),Ga,K,Ka=r((()=>{G(),Ga=B(`×`),Jp=B(``),Yp=B(``),Xp=B(`✨`),Zp=B(`
`),Qp=()=>{let e,t,n=O(()=>{let e=L();return e.kind===`focused`?e.fiber:null});D(()=>{let n=Lp();$e(()=>{if(L().kind!==`focused`||!e||!t)return;let{totalUpdates:r,currentIndex:i,updates:a,isVisible:o,windowOffset:s}=n,c=Math.max(0,r-1),l=o?`#${s+i} Re-render`:c>0?`×${c}`:``,u;if(c>0&&i>=0&&i0?e<.1-2**-52?`< 0.1ms`:`${Number(e.toFixed(1))}ms`:void 0}e.dataset.text=l?` • ${l}`:``,t.dataset.text=u?` • ${u}`:``})});let r=O(()=>{let e=n();if(!e)return null;let{name:t,wrappers:r,wrapperTypes:i}=Ms(e),a=r.length?`${r.join(`(`)}(${t})${`)`.repeat(r.length)}`:t==null?``:t,o=i[0];return(()=>{var e=Jp(),n=e.firstChild;return V(e,`title`,a),U(e,t==null?`Unknown`:t,n),U(n,A(j,{when:o,children:e=>[(()=>{var t=Yp();return U(t,()=>e().type),E(()=>H(t,X(`rounded py-[1px] px-1`,`truncate`,e().compiler&&`bg-purple-800 text-neutral-400`,!e().compiler&&`bg-neutral-700 text-neutral-300`,e().type===`memo`&&`bg-[#5f3f9a] text-white`))),t})(),A(j,{get when(){return e().compiler},get children(){return Xp()}})]})),U(e,A(j,{get when(){return i.length>1},get children(){var e=qp();return e.firstChild,U(e,()=>i.length-1,null),e}}),null),E(()=>V(n,`title`,o==null?void 0:o.title)),e})()});return(()=>{var n=Zp(),i=n.firstChild,a=i.firstChild,o=a.nextSibling;U(n,r,i);var s=e;typeof s==`function`?wa(s,a):e=a;var c=t;return typeof c==`function`?wa(c,o):t=o,n})()}})),em,tm,nm,rm=r((()=>{G(),I(),In(),Ka(),na(),gc(),Ks(),Tp(),Dp(),Z(),kp(),jp(),Np(),$p(),em=B(`Array()`),uh=B(``),dh=B(`
`),fh=B(`
:`),ph=B(`
:`),mh=B(`
`),hh=B(`
`),gh=B(``),_h=B(``),vh=B(`
`),yh=e=>(()=>{var t=lh(),n=t.firstChild,r=n.nextSibling,i=r.firstChild.nextSibling;return i.nextSibling,ya(n,`click`,e.onToggle,!0),U(n,A(K,{name:`icon-chevron-right`,size:12,get class(){return X(`transition-[color,transform]`,e.isNegative?`text-[#f87171]`:`text-[#4ade80]`,e.expanded&&`rotate-90`)}})),U(r,()=>e.length,i),t})(),bh=e=>{let[t,n]=T(!1),r=O(()=>e.value!==null&&typeof e.value==`object`&&!(e.value instanceof Date)),i=O(()=>r()?Object.keys(e.value):[]),a=t=>r()?e.value[t]:void 0;return A(j,{get when(){return r()},get fallback(){return(()=>{var t=ph(),n=t.firstChild,r=n.firstChild,i=n.nextSibling;return U(n,()=>e.path,r),U(i,()=>xm(e.value)),t})()},get children(){var r=fh(),o=r.firstChild,s=o.firstChild,c=s.nextSibling,l=c.firstChild;return s.$$click=()=>n(e=>!e),U(s,A(K,{name:`icon-chevron-right`,size:12,get class(){return X(`transition-[color,transform]`,e.isNegative?`text-[#f87171]`:`text-[#4ade80]`,t()&&`rotate-90`)}})),U(c,()=>e.path,l),U(o,A(j,{get when(){return!t()},get children(){var t=uh();return U(t,(()=>{var t=W(()=>e.value instanceof Date);return()=>t()?xm(e.value):`{${Object.keys(e.value).join(`, `)}}`})()),t}}),null),U(r,A(j,{get when(){return t()},get children(){var t=dh();return U(t,A(Rt,{get each(){return i()},children:t=>A(bh,{get value(){return a(t)},path:t,get isNegative(){return e.isNegative}})})),t}}),null),r}})},xh=e=>{let t=O(()=>Sm(e.value)),n=()=>t().value,r=()=>n()!==null&&typeof n()==`object`&&!(n()instanceof Promise),i=O(()=>r()&&!Array.isArray(n())?Object.keys(n()):[]),a=e=>n()[e];return A(j,{get when(){return!t().error},get fallback(){return(()=>{var e=gh();return U(e,()=>t().error),e})()},get children(){return A(j,{get when(){return r()},get fallback(){return(()=>{var e=_h();return U(e,()=>xm(n())),e})()},get children(){return A(j,{get when(){return Array.isArray(n())},get fallback(){return(()=>{var t=vh(),r=t.firstChild,o=r.nextSibling;return ya(r,`click`,e.onToggle,!0),U(r,A(K,{name:`icon-chevron-right`,size:12,get class(){return X(`transition-[color,transform]`,e.isNegative?`text-[#f87171]`:`text-[#4ade80]`,e.expanded&&`rotate-90`)}})),U(o,A(j,{get when(){return e.expanded},get fallback(){return(()=>{var e=_h();return U(e,()=>xm(n())),e})()},get children(){var t=mh();return U(t,A(Rt,{get each(){return i()},children:t=>A(bh,{get value(){return a(t)},path:t,get isNegative(){return e.isNegative}})})),t}})),U(t,A(sh,{get text(){return gm(n())},class:`absolute top-0.5 right-0.5 opacity-0 transition-opacity group-hover:opacity-100 self-end`,children:({ClipboardIcon:e})=>e}),null),E(()=>H(r,X(`flex items-center`,`p-0 mt-0.5 mr-1`,`opacity-50`))),t})()},get children(){var t=hh();return U(t,A(yh,{get length(){return n().length},get expanded(){return e.expanded},get onToggle(){return e.onToggle},get isNegative(){return e.isNegative}}),null),U(t,A(j,{get when(){return e.expanded},get children(){var t=mh();return U(t,A(zt,{get each(){return n()},children:(t,n)=>A(bh,{get value(){return t()},get path(){return n.toString()},get isNegative(){return e.isNegative}})})),t}}),null),U(t,A(sh,{get text(){return gm(n())},class:`absolute top-0.5 right-0.5 opacity-0 transition-opacity group-hover:opacity-100 self-end`,children:({ClipboardIcon:e})=>e}),null),t}})}})}})},ga([`click`])})),Ch,wh,Th,Eh,Dh,Oh,kh,Ah,jh,Mh,Nh=r((()=>{I(),In(),Ge(),xn(),Ch=50,wh=e=>{switch(e.kind){case`initialized`:return e.changes.currentValue;case`partially-initialized`:return e.value}},Th=(e,t)=>{for(let n of e){let e=t.get(n.name);if(e){t.set(e.name,{count:e.count+1,currentValue:n.value,id:e.name,lastUpdated:Date.now(),name:e.name,previousValue:n.prevValue});continue}t.set(n.name,{count:1,currentValue:n.value,id:n.name,lastUpdated:Date.now(),name:n.name,previousValue:n.prevValue})}},Eh=(e,t)=>{for(let n of e){let e=t.contextChanges.get(n.contextType);if(e){if(_n(wh(e),n.value))continue;if(e.kind===`partially-initialized`){t.contextChanges.set(n.contextType,{kind:`initialized`,changes:{count:1,currentValue:n.value,id:n.contextType.toString(),lastUpdated:Date.now(),name:n.name,previousValue:e.value}});continue}t.contextChanges.set(n.contextType,{kind:`initialized`,changes:{count:e.changes.count+1,currentValue:n.value,id:n.contextType.toString(),lastUpdated:Date.now(),name:n.name,previousValue:e.changes.currentValue}});continue}t.contextChanges.set(n.contextType,{kind:`partially-initialized`,id:n.contextType.toString(),lastUpdated:Date.now(),name:n.name,value:n.value})}},Dh=e=>{let t={contextChanges:new Map,propsChanges:new Map,stateChanges:new Map};return e.forEach(e=>{Eh(e.contextChanges,t),Th(e.stateChanges,t.stateChanges),Th(e.propsChanges,t.propsChanges)}),t},Oh=(e,t)=>{let n=new Map;return e.forEach((e,t)=>{n.set(t,e)}),t.forEach((e,t)=>{let r=n.get(t);if(!r){n.set(t,e);return}n.set(t,{count:r.count+e.count,currentValue:e.currentValue,id:e.id,lastUpdated:e.lastUpdated,name:e.name,previousValue:e.previousValue})}),n},kh=(e,t)=>{let n=new Map;return e.contextChanges.forEach((e,t)=>{n.set(t,e)}),t.contextChanges.forEach((e,t)=>{let r=n.get(t);if(!r){n.set(t,e);return}if(wh(e)!==wh(r))switch(r.kind){case`initialized`:switch(e.kind){case`initialized`:n.set(t,{kind:`initialized`,changes:{...e.changes,count:e.changes.count+r.changes.count+1,currentValue:e.changes.currentValue,previousValue:e.changes.previousValue}});return;case`partially-initialized`:n.set(t,{kind:`initialized`,changes:{count:r.changes.count+1,currentValue:e.value,id:e.id,lastUpdated:e.lastUpdated,name:e.name,previousValue:r.changes.currentValue}});return}case`partially-initialized`:switch(e.kind){case`initialized`:n.set(t,{kind:`initialized`,changes:{count:e.changes.count+1,currentValue:e.changes.currentValue,id:e.changes.id,lastUpdated:e.changes.lastUpdated,name:e.changes.name,previousValue:r.value}});return;case`partially-initialized`:n.set(t,{kind:`initialized`,changes:{count:1,currentValue:e.value,id:e.id,lastUpdated:e.lastUpdated,name:e.name,previousValue:r.value}});return}}}),n},Ah=(e,t)=>({contextChanges:kh(e,t),propsChanges:Oh(e.propsChanges,t.propsChanges),stateChanges:Oh(e.stateChanges,t.stateChanges)}),jh=e=>Array.from(e.propsChanges.values()).reduce((e,t)=>e+t.count,0)+Array.from(e.stateChanges.values()).reduce((e,t)=>e+t.count,0)+Array.from(e.contextChanges.values()).filter(e=>e.kind===`initialized`).reduce((e,t)=>e+t.changes.count,0),Mh=e=>{let t={queue:[]},[n,r]=T({propsChanges:new Map,stateChanges:new Map,contextChanges:new Map}),i=O(()=>{let e=L();return e.kind===`focused`?Ee(e.fiber):null});return tt(()=>{let n=setInterval(()=>{t.queue.length!==0&&(r(n=>{var r;let i=Ah(n,Dh(t.queue)),a=jh(n),o=jh(i)-a;return e==null||(r=e.onChangeUpdate)==null||r.call(e,o),i}),t.queue=[])},Ch);k(()=>{clearInterval(n)})}),D(()=>{let e=i();if(!e)return;let n=e=>{t.queue.push(e)},a=Fn.changesListeners.get(e);a||(a=[],Fn.changesListeners.set(e,a)),a.push(n),k(()=>{var i,a;r({propsChanges:new Map,stateChanges:new Map,contextChanges:new Map}),t.queue=[],Fn.changesListeners.set(e,(i=(a=Fn.changesListeners.get(e))==null?void 0:a.filter(e=>e!==n))==null?[]:i)})}),n}})),Ph,Fh,Ih,Lh,Rh,zh,Bh,Vh,Hh,Uh,Wh,Gh,Kh,qh,Jh,Yh,Xh,Zh,Qh,$h,eg,tg,ng,rg,ig,ag,og,sg,cg=r((()=>{G(),I(),ch(),Ka(),Z(),Sh(),Kp(),wm(),Nh(),Ge(),In(),Ph=B(`
No changes detected since selecting
The props, state, and context changes within your component will be reported here`),Fh=B(`
Why did render?
`),Ih=B(` hook called in `),Lh=B(`
`),Bh=B(`
`),Vh=B(`
`),Hh=B(`
`),Uh=B(`
-
+`),Wh=B(``),Gh=B(`
`),Kh=B(`
Function reference changed`),qh=B(``),Jh=B(`
`),Yh=B(`
-`),Xh=B(`
+`),Zh=B(`
Reference changed but objects are structurally the same`),Qh=B(`
x`),$h=()=>{let[e,t]=T(!0),n=Mh(),[r,i]=T(!1),a=O(()=>jh(n())>0);D(()=>{if(!r()&&a()){let e=setTimeout(()=>{i(!0),requestAnimationFrame(()=>{t(!0)})},0);k(()=>clearTimeout(e))}});let o=O(()=>new Map(Array.from(n().contextChanges.entries()).flatMap(([e,t])=>t.kind===`initialized`?[[e,t.changes]]:[]))),s=O(()=>{let e=L();return e.kind===`focused`?e.fiber:null});return A(j,{get when(){return s()},children:t=>[A(tg,{}),(()=>{var r=Fh(),i=r.firstChild,s=i.firstChild.firstChild.nextSibling,c=i.nextSibling;return U(s,()=>ye(t())),U(i,A(j,{get when(){return!a()},get children(){return Ph()}}),null),U(c,A(rg,{get changes(){return n().propsChanges},title:`Changed Props`,get isExpanded(){return e()}}),null),U(c,A(rg,{renderName:e=>{var n;return eg(e,(n=ye(ve(t())))==null?`Unknown Component`:n)},get changes(){return n().stateChanges},title:`Changed State`,get isExpanded(){return e()}}),null),U(c,A(rg,{get changes(){return o()},title:`Changed Context`,get isExpanded(){return e()}}),null),E(()=>H(c,X(`flex flex-col gap-y-2 pl-3 relative overflow-y-auto h-full`))),r})()]})},eg=(e,t)=>{if(Number.isNaN(Number(e)))return e;let n=Number.parseInt(e),r=e=>{let t=e%10,n=e%100;if(n>=11&&n<=13)return`th`;switch(t){case 1:return`st`;case 2:return`nd`;case 3:return`rd`;default:return`th`}};return(()=>{var e=Ih(),i=e.firstChild,a=i.firstChild,o=i.nextSibling.firstChild.nextSibling;return U(i,n,a),U(i,()=>r(n),a),U(o,t),e})()},tg=()=>{let e,t,n,r={isPropsChanged:!1,isStateChanged:!1,isContextChanged:!1},i=ks(()=>{let r=[];(e==null?void 0:e.dataset.flash)===`true`&&r.push(e),(t==null?void 0:t.dataset.flash)===`true`&&r.push(t),(n==null?void 0:n.dataset.flash)===`true`&&r.push(n);for(let e of r)e.classList.remove(`count-flash-white`),e.offsetWidth,e.classList.add(`count-flash-white`)},400);return D(et(Lp,a=>{var o,s,c,l,u,d;if(!e||!t||!n)return;let{currentIndex:f,updates:p}=a,m=p[f];!m||f===0||(i(),r={isPropsChanged:((o=(s=m.props)==null||(s=s.changes)==null?void 0:s.size)==null?0:o)>0,isStateChanged:((c=(l=m.state)==null||(l=l.changes)==null?void 0:l.size)==null?0:c)>0,isContextChanged:((u=(d=m.context)==null||(d=d.changes)==null?void 0:d.size)==null?0:u)>0},e.dataset.flash!==`true`&&(e.dataset.flash=r.isPropsChanged.toString()),t.dataset.flash!==`true`&&(t.dataset.flash=r.isStateChanged.toString()),n.dataset.flash!==`true`&&(n.dataset.flash=r.isContextChanged.toString()))},{defer:!0})),(()=>{var r=Lh(),i=r.firstChild,a=i.firstChild.firstChild.firstChild.nextSibling,o=a.firstChild,s=o.nextSibling,c=s.nextSibling,l=e;typeof l==`function`?wa(l,o):e=o;var u=t;typeof u==`function`?wa(u,s):t=s;var d=n;return typeof d==`function`?wa(d,c):n=c,E(e=>{var t=X(`react-section-header`,`overflow-hidden`,`max-h-0`,`transition-[max-height]`),n=X(`flex-1 react-scan-expandable`),o=X(`ml-auto`,`change-scope`,`transition-opacity duration-300 delay-150`);return t!==e.e&&H(r,e.e=t),n!==e.t&&H(i,e.t=n),o!==e.a&&H(a,e.a=o),e},{e:void 0,t:void 0,a:void 0}),r})()},ng=e=>e,rg=e=>{let[t,n]=T(new Set),[r,i]=T(new Set),a=O(()=>Array.from(e.changes.keys())),o=t=>{var n;return((n=e.renderName)==null?ng:n)(t)};return A(j,{get when(){return e.changes.size>0},get children(){var s=Rh(),c=s.firstChild,l=c.nextSibling;return U(c,()=>e.title),U(l,A(Rt,{get each(){return a()},children:a=>{let s=()=>r().has(String(a)),c=O(()=>e.changes.get(a)),l=O(()=>{var e;return Sm((e=c())==null?void 0:e.previousValue)}),u=O(()=>{var e;return Sm((e=c())==null?void 0:e.currentValue)}),d=O(()=>vm(l().value,u().value));return A(j,{get when(){return c()},children:r=>(()=>{var c=zh(),f=c.firstChild,p=f.firstChild,m=p.firstChild,h=f.nextSibling,g=h.firstChild.firstChild;return f.$$click=()=>{i(e=>{let t=new Set(e);return t.has(String(a))?t.delete(String(a)):t.add(String(a)),t})},U(p,A(K,{name:`icon-chevron-right`,size:12,get class(){return X(`text-[#666] transition-transform duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]`,{"rotate-90":s()})}}),m),U(m,()=>o(r().name),null),U(m,A(sg,{get count(){return r().count},get isFunction(){return typeof r().currentValue==`function`},get showWarning(){return d().changes.length===0},forceFlash:!0}),null),U(g,A(j,{get when(){return W(()=>!l().error)()&&!u().error},get fallback(){return A(ig,{get currError(){return u().error},get prevError(){return l().error}})},get children(){return A(j,{get when(){return d().changes.length>0},get fallback(){return A(og,{get currValue(){return u().value},entryKey:a,get expandedFunctions(){return t()},get prevValue(){return l().value},setExpandedFunctions:n})},get children(){return A(ag,{get change(){return r()},get diff(){return d()},get expandedFunctions(){return t()},renderName:o,setExpandedFunctions:n,get title(){return e.title}})}})}})),E(()=>H(h,X(`react-scan-expandable`,{"react-scan-expanded":s()}))),c})()})}})),s}})},ig=e=>[A(j,{get when(){return e.prevError},get children(){var t=Bh();return U(t,()=>e.prevError),t}}),A(j,{get when(){return e.currError},get children(){var t=Vh();return U(t,()=>e.currError),t}})],ag=e=>A(zt,{get each(){return e.diff.changes},children:(t,n)=>{let r=O(()=>Sm(t().prevValue)),i=O(()=>Sm(t().currentValue)),a=()=>typeof r().value==`function`||typeof i().value==`function`,o=n=>{let r=`${ym(t().path)}-${n}`;e.setExpandedFunctions(e=>{let t=new Set(e);return t.has(r)?t.delete(r):t.add(r),t})},s=(e,t)=>{t.key!==`Enter`&&t.key!==` `||(t.preventDefault(),o(e))},c=()=>e.title===`Props`?t().path.length>0?`${e.renderName(String(e.change.name))}.${ym(t().path)}`:void 0:e.title===`State`&&t().path.length>0?`state.${ym(t().path)}`:ym(t().path);return(()=>{var l=Uh(),u=l.firstChild,d=u.firstChild.nextSibling,f=u.nextSibling,p=f.firstChild.nextSibling;return U(l,A(j,{get when(){return c()},get children(){var e=Hh();return U(e,c),e}}),u),ya(u,`keydown`,a()?e=>s(`prev`,e):void 0,!0),ya(u,`click`,a()?()=>o(`prev`):void 0,!0),U(d,(()=>{var n=W(()=>!!r().error);return()=>n()?(()=>{var e=Wh();return U(e,()=>r().error),e})():W(()=>!!a())()?(()=>{var n=Gh(),a=n.firstChild,o=a.firstChild;return U(o,()=>bm(r().value,e.expandedFunctions.has(`${ym(t().path)}-prev`))),U(a,(()=>{var e=W(()=>typeof r().value==`function`);return()=>e()&&A(sh,{get text(){return String(r().value)},class:`opacity-0 transition-opacity group-hover:opacity-100`,children:({ClipboardIcon:e})=>e})})(),null),U(n,(()=>{var e=W(()=>{var e,t;return((e=r().value)==null?void 0:e.toString())===((t=i().value)==null?void 0:t.toString())});return()=>e()&&Kh()})(),null),n})():A(xh,{get value(){return r().value},get expanded(){return e.expandedFunctions.has(`${ym(t().path)}-prev`)},onToggle:()=>{let n=`${ym(t().path)}-prev`;e.setExpandedFunctions(e=>{let t=new Set(e);return t.has(n)?t.delete(n):t.add(n),t})},isNegative:!0})})()),ya(f,`keydown`,a()?e=>s(`current`,e):void 0,!0),ya(f,`click`,a()?()=>o(`current`):void 0,!0),U(p,(()=>{var n=W(()=>!!i().error);return()=>n()?(()=>{var e=qh();return U(e,()=>i().error),e})():W(()=>!!a())()?(()=>{var n=Jh(),a=n.firstChild,o=a.firstChild;return U(o,()=>bm(i().value,e.expandedFunctions.has(`${ym(t().path)}-current`))),U(a,(()=>{var e=W(()=>typeof i().value==`function`);return()=>e()&&A(sh,{get text(){return String(i().value)},class:`opacity-0 transition-opacity group-hover:opacity-100`,children:({ClipboardIcon:e})=>e})})(),null),U(n,(()=>{var e=W(()=>{var e,t;return((e=r().value)==null?void 0:e.toString())===((t=i().value)==null?void 0:t.toString())});return()=>e()&&Kh()})(),null),n})():A(xh,{get value(){return i().value},get expanded(){return e.expandedFunctions.has(`${ym(t().path)}-current`)},onToggle:()=>{let n=`${ym(t().path)}-current`;e.setExpandedFunctions(e=>{let t=new Set(e);return t.has(n)?t.delete(n):t.add(n),t})},isNegative:!1})})()),E(t=>{var r=X(`flex flex-col gap-y-1`,n[(()=>{var t=Yh(),n=t.firstChild.nextSibling;return U(n,A(xh,{get value(){return e.prevValue},get expanded(){return e.expandedFunctions.has(`${String(e.entryKey)}-prev`)},onToggle:()=>{let t=`${String(e.entryKey)}-prev`;e.setExpandedFunctions(e=>{let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n})},isNegative:!0})),t})(),(()=>{var t=Xh(),n=t.firstChild.nextSibling;return U(n,A(xh,{get value(){return e.currValue},get expanded(){return e.expandedFunctions.has(`${String(e.entryKey)}-current`)},onToggle:()=>{let t=`${String(e.entryKey)}-current`;e.setExpandedFunctions(e=>{let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n})},isNegative:!1})),t})(),A(j,{get when(){return W(()=>typeof e.currValue==`object`)()&&e.currValue!==null},get children(){var e=Zh(),t=e.firstChild;return U(e,A(K,{name:`icon-triangle-alert`,class:`text-yellow-500 mb-px`,size:14}),t),e}})],sg=e=>{let t=!0,n,r=e.count;return D(()=>{let t=e.count,i=n;!i||r===t||(i.classList.remove(`count-flash`),i.offsetWidth,i.classList.add(`count-flash`),r=t)}),D(()=>{let r=e.forceFlash;if(t){t=!1;return}if(r){let e=setTimeout(()=>{n==null||n.classList.add(`count-flash-white`),e=setTimeout(()=>{n==null||n.classList.remove(`count-flash-white`)},300)},500);k(()=>{clearTimeout(e)})}}),(()=>{var t=Qh(),r=t.firstChild,i=n;return typeof i==`function`?wa(i,t):n=t,U(t,A(j,{get when(){return e.showWarning},get children(){return A(K,{name:`icon-triangle-alert`,class:`text-yellow-500 mb-px`,size:14})}}),r),U(t,A(j,{get when(){return e.isFunction},get children(){return A(K,{name:`icon-function`,class:`text-[#A855F7] mb-px`,size:14})}}),r),U(t,()=>e.count,null),t})()},ga([`click`,`keydown`])})),lg,ug,dg,fg,pg,mg,hg,gg=r((()=>{G(),I(),In(),Ka(),Ks(),Z(),th(),ah(),Kp(),Lr(),wm(),cg(),lg=B(`
Something went wrong in the inspector
How to stop renders
Stop the following props, state and context from changing between renders, and wrap the component in React.memo if not already`),iy=B(`
No changes detected
This component would not have rendered if it was memoized`),ay=B(`
• Renders: x
Changed Props
Changed State
Changed Context`),oy=B(`
• Render time: ms`),sy=B(`
No changes`),cy=B(`
/x`),ly=B(`
index
/x`),uy=e=>{let{setRoute:t}=Hg(),[n,r]=T(!0),i=Rx();tt(()=>{let e=localStorage.getItem(`react-scan-tip-shown`),t=e===`true`?!0:e===`false`?!1:null;if(t===null){r(!0),localStorage.setItem(`react-scan-tip-is-shown`,`true`);return}t||r(!1)});let a=()=>e.selectedFiber.changes.context.length===0&&e.selectedFiber.changes.props.length===0&&e.selectedFiber.changes.state.length===0;return(()=>{var o=ay(),s=o.firstChild,c=s.firstChild,l=c.firstChild,u=c.nextSibling,d=u.firstChild,f=d.firstChild,p=d.nextSibling,m=p.firstChild,h=m.firstChild.nextSibling;h.nextSibling;var g=s.nextSibling,_=g.firstChild,v=_.firstChild,y=_.nextSibling,b=y.firstChild,ee=y.nextSibling,te=ee.firstChild;return c.$$click=()=>{t({route:`render-visualization`,routeMessage:null})},U(c,A(o_,{size:14}),l),U(f,()=>e.selectedFiber.name),U(p,!i&&(()=>{var t=oy(),n=t.firstChild.nextSibling;return n.nextSibling,U(t,()=>e.selectedFiber.totalTime.toFixed(0),n),E(()=>H(t,X([`text-xs text-gray-400`]))),t})(),m),U(m,()=>e.selectedFiber.count,h),U(o,A(j,{get when(){return W(()=>!!n())()&&!a()},get children(){var e=ry(),t=e.firstChild,n=t.nextSibling,i=n.nextSibling,a=i.firstChild,o=a.nextSibling;return t.$$click=()=>{r(!1),localStorage.setItem(`react-scan-tip-shown`,`false`)},U(t,A(r_,{size:12})),E(r=>{var s=X([`w-full mb-4 bg-[#0A0A0A] border border-[#27272A] rounded-sm overflow-hidden flex relative`]),c=X([`absolute right-2 top-2 rounded-sm p-1 hover:bg-[#18181B]`]),l=X([`w-1 bg-[#d36cff]`]),u=X([`flex-1`]),d=X([`px-3 py-2 text-gray-100 text-xs font-semibold`]),f=X([`px-3 pb-2 text-gray-400 text-[10px]`]);return s!==r.e&&H(e,r.e=s),c!==r.t&&H(t,r.t=c),l!==r.a&&H(n,r.a=l),u!==r.o&&H(i,r.o=u),d!==r.i&&H(a,r.i=d),f!==r.n&&H(o,r.n=f),r},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0}),e}}),g),U(o,A(j,{get when(){return a()},get children(){var e=iy(),t=e.firstChild,n=t.nextSibling,r=n.firstChild,i=r.nextSibling;return E(a=>{var o=X([`w-full mb-4 bg-[#0A0A0A] border border-[#27272A] rounded-sm overflow-hidden flex`]),s=X([`w-1 bg-[#d36cff]`]),c=X([`flex-1`]),l=X([`px-3 py-2 text-gray-100 text-sm font-semibold`]),u=X([`px-3 pb-2 text-gray-400 text-xs`]);return o!==a.e&&H(e,a.e=o),s!==a.t&&H(t,a.t=s),c!==a.a&&H(n,a.a=c),l!==a.o&&H(r,a.o=l),u!==a.i&&H(i,a.i=u),a},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),e}}),g),U(_,A(j,{get when(){return e.selectedFiber.changes.props.length>0},get fallback(){return(()=>{var e=sy();return E(()=>H(e,X([`flex items-center justify-center h-full bg-[#0A0A0A] text-[#A1A1AA] border-t border-[#27272A]`]))),e})()},get children(){return A(Rt,{get each(){return e.selectedFiber.changes.props.toSorted((e,t)=>t.count-e.count)},children:t=>(()=>{var n=cy(),r=n.firstChild,i=r.nextSibling,a=i.firstChild,o=a.nextSibling;return o.nextSibling,U(r,()=>t.name),U(i,()=>t.count,a),U(i,()=>e.selectedFiber.count,o),E(e=>{var t=X([`flex flex-col justify-between items-center border-t overflow-x-auto border-[#27272A] px-1 py-1 text-wrap bg-[#0A0A0A] text-[10px]`]),a=X([`text-white `]),o=X([` text-[8px] text-[#d36cff] pl-1 py-1 `]);return t!==e.e&&H(n,e.e=t),a!==e.t&&H(r,e.t=a),o!==e.a&&H(i,e.a=o),e},{e:void 0,t:void 0,a:void 0}),n})()})}}),null),U(y,A(j,{get when(){return e.selectedFiber.changes.state.length>0},get fallback(){return(()=>{var e=sy();return E(()=>H(e,X([`flex items-center justify-center h-full bg-[#0A0A0A] text-[#A1A1AA] border-t border-[#27272A]`]))),e})()},get children(){return A(Rt,{get each(){return e.selectedFiber.changes.state.toSorted((e,t)=>t.count-e.count)},children:t=>(()=>{var n=ly(),r=n.firstChild;r.firstChild;var i=r.nextSibling,a=i.firstChild,o=a.nextSibling;return o.nextSibling,U(r,()=>t.index,null),U(i,()=>t.count,a),U(i,()=>e.selectedFiber.count,o),E(e=>{var t=X([`flex flex-col justify-between items-center border-t overflow-x-auto border-[#27272A] px-1 py-1 text-wrap bg-[#0A0A0A] text-[10px]`]),a=X([`text-white `]),o=X([`rounded-full text-[#d36cff] pl-1 py-1 text-[8px]`]);return t!==e.e&&H(n,e.e=t),a!==e.t&&H(r,e.t=a),o!==e.a&&H(i,e.a=o),e},{e:void 0,t:void 0,a:void 0}),n})()})}}),null),U(ee,A(j,{get when(){return e.selectedFiber.changes.context.length>0},get fallback(){return(()=>{var e=sy();return E(()=>H(e,X([`flex items-center justify-center h-full bg-[#0A0A0A] text-[#A1A1AA] border-t border-[#27272A] py-2`]))),e})()},get children(){return A(Rt,{get each(){return e.selectedFiber.changes.context.toSorted((e,t)=>t.count-e.count)},children:t=>(()=>{var n=cy(),r=n.firstChild,i=r.nextSibling,a=i.firstChild,o=a.nextSibling;return o.nextSibling,U(r,()=>t.name),U(i,()=>t.count,a),U(i,()=>e.selectedFiber.count,o),E(e=>{var t=X([`flex flex-col justify-between items-center border-t border-[#27272A] px-1 py-1 bg-[#0A0A0A] text-[10px] overflow-x-auto`]),a=X([`text-white `]),o=X([`rounded-full text-[#d36cff] pl-1 py-1 text-[8px] text-wrap`]);return t!==e.e&&H(n,e.e=t),a!==e.t&&H(r,e.t=a),o!==e.a&&H(i,e.a=o),e},{e:void 0,t:void 0,a:void 0}),n})()})}}),null),E(e=>{var t=X([`w-full min-h-fit h-full flex flex-col py-4 pt-0 rounded-sm`]),n=X([`flex items-start gap-x-4 `]),r=X([`text-white hover:bg-[#34343b] flex gap-x-1 justify-center items-center mb-4 w-fit px-2.5 py-1.5 text-xs rounded-sm bg-[#18181B]`]),i=X([`flex flex-col gap-y-1`]),a=X([`text-sm font-bold text-white overflow-x-hidden`]),l=X([`flex gap-x-2`]),f=X([`text-xs text-gray-400 mb-4`]),h=X([`flex w-full`]),ne=X([`flex flex-col border border-[#27272A] rounded-l-sm overflow-hidden w-1/3`]),x=X([`text-[14px] font-semibold px-2 py-2 bg-[#18181B] text-white flex justify-center`]),S=X([`flex flex-col border border-[#27272A] border-l-0 overflow-hidden w-1/3`]),re=X([` text-[14px] font-semibold px-2 py-2 bg-[#18181B] text-white flex justify-center`]),ie=X([`flex flex-col border border-[#27272A] border-l-0 rounded-r-sm overflow-hidden w-1/3`]),ae=X([` text-[14px] font-semibold px-2 py-2 bg-[#18181B] text-white flex justify-center`]);return t!==e.e&&H(o,e.e=t),n!==e.t&&H(s,e.t=n),r!==e.a&&H(c,e.a=r),i!==e.o&&H(u,e.o=i),a!==e.i&&H(d,e.i=a),l!==e.n&&H(p,e.n=l),f!==e.s&&H(m,e.s=f),h!==e.h&&H(g,e.h=h),ne!==e.r&&H(_,e.r=ne),x!==e.d&&H(v,e.d=x),S!==e.l&&H(y,e.l=S),re!==e.u&&H(b,e.u=re),ie!==e.c&&H(ee,e.c=ie),ae!==e.w&&H(te,e.w=ae),e},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0,h:void 0,r:void 0,d:void 0,l:void 0,u:void 0,c:void 0,w:void 0}),o})()},ga([`click`])})),fy,py,my,hy,gy,_y,vy,yy=r((()=>{G(),I(),xn(),Ks(),Z(),Ug(),d_(),y_(),W_(),ny(),dy(),fy=B(`

Click on an item in the History list to get started`),py=B(`

Scanning for slowdowns

You don't need to keep this panel open for React Scan to record slowdowns

Enable audio alerts to hear a delightful ding every time a large slowdown is recorded