This commit is contained in:
2026-03-18 20:54:43 +01:00
parent b3c8b77f12
commit 9fe656b34c
8058 changed files with 912898 additions and 23 deletions
@@ -0,0 +1,203 @@
import {
createContext,
createEffect,
createMemo,
createSignal,
onCleanup,
useContext,
} from 'solid-js'
import { clearDelegatedEvents, delegateEvents } from 'solid-js/web'
import { PIP_DEFAULT_HEIGHT } from '../constants'
import { useQueryDevtoolsContext } from './QueryDevtoolsContext'
import type { Accessor, JSX } from 'solid-js'
import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
interface PiPProviderProps {
children: JSX.Element
localStore: StorageObject<string>
setLocalStore: StorageSetter<string, unknown>
disabled?: boolean
}
type PiPContextType = {
pipWindow: Window | null
requestPipWindow: (width: number, height: number) => void
closePipWindow: () => void
disabled: boolean
}
class PipOpenError extends Error {}
const PiPContext = createContext<Accessor<PiPContextType> | undefined>(
undefined,
)
export const PiPProvider = (props: PiPProviderProps) => {
// Expose pipWindow that is currently active
const [pipWindow, setPipWindow] = createSignal<Window | null>(null)
// Close pipWindow programmatically
const closePipWindow = () => {
const w = pipWindow()
if (w != null) {
w.close()
setPipWindow(null)
}
}
// Open new pipWindow
const requestPipWindow = (width: number, height: number) => {
// We don't want to allow multiple requests.
if (pipWindow() != null) {
return
}
const pip = window.open(
'',
'TSQD-Devtools-Panel',
`width=${width},height=${height},popup`,
)
if (!pip) {
throw new PipOpenError(
'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.',
)
}
// Remove existing styles
pip.document.head.innerHTML = ''
// Remove existing body
pip.document.body.innerHTML = ''
// Clear Delegated Events
clearDelegatedEvents(pip.document)
pip.document.title = 'TanStack Query Devtools'
pip.document.body.style.margin = '0'
// Detect when window is closed by user
pip.addEventListener('pagehide', () => {
props.setLocalStore('pip_open', 'false')
setPipWindow(null)
})
// It is important to copy all parent window styles. Otherwise, there would be no CSS available at all
// https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window
;[
...(useQueryDevtoolsContext().shadowDOMTarget || document).styleSheets,
].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules]
.map((rule) => rule.cssText)
.join('')
const style = document.createElement('style')
const style_node = styleSheet.ownerNode
let style_id = ''
if (style_node && 'id' in style_node) {
style_id = style_node.id
}
if (style_id) {
style.setAttribute('id', style_id)
}
style.textContent = cssRules
pip.document.head.appendChild(style)
} catch (e) {
const link = document.createElement('link')
if (styleSheet.href == null) {
return
}
link.rel = 'stylesheet'
link.type = styleSheet.type
link.media = styleSheet.media.toString()
link.href = styleSheet.href
pip.document.head.appendChild(link)
}
})
delegateEvents(
[
'focusin',
'focusout',
'pointermove',
'keydown',
'pointerdown',
'pointerup',
'click',
'mousedown',
'input',
],
pip.document,
)
props.setLocalStore('pip_open', 'true')
setPipWindow(pip)
}
createEffect(() => {
const pip_open = (props.localStore.pip_open ?? 'false') as 'true' | 'false'
if (pip_open === 'true' && !props.disabled) {
try {
requestPipWindow(
Number(window.innerWidth),
Number(props.localStore.height || PIP_DEFAULT_HEIGHT),
)
} catch (error) {
if (error instanceof PipOpenError) {
console.error(error.message)
props.setLocalStore('pip_open', 'false')
props.setLocalStore('open', 'false')
return
}
throw error
}
}
})
createEffect(() => {
// Setup mutation observer for goober styles with id `_goober
const gooberStyles = (
useQueryDevtoolsContext().shadowDOMTarget || document
).querySelector('#_goober')
const w = pipWindow()
if (gooberStyles && w) {
const observer = new MutationObserver(() => {
const pip_style = (
useQueryDevtoolsContext().shadowDOMTarget || w.document
).querySelector('#_goober')
if (pip_style) {
pip_style.textContent = gooberStyles.textContent
}
})
observer.observe(gooberStyles, {
childList: true, // observe direct children
subtree: true, // and lower descendants too
characterDataOldValue: true, // pass old data to callback
})
onCleanup(() => {
observer.disconnect()
})
}
})
const value = createMemo(() => ({
pipWindow: pipWindow(),
requestPipWindow,
closePipWindow,
disabled: props.disabled ?? false,
}))
return (
<PiPContext.Provider value={value}>{props.children}</PiPContext.Provider>
)
}
export const usePiPWindow = () => {
const context = createMemo(() => {
const ctx = useContext(PiPContext)
if (!ctx) {
throw new Error('usePiPWindow must be used within a PiPProvider')
}
return ctx()
})
return context
}
@@ -0,0 +1,47 @@
import { createContext, useContext } from 'solid-js'
import type { Query, QueryClient, onlineManager } from '@tanstack/query-core'
type XPosition = 'left' | 'right'
type YPosition = 'top' | 'bottom'
export type DevtoolsPosition = XPosition | YPosition
export type DevtoolsButtonPosition = `${YPosition}-${XPosition}` | 'relative'
export type Theme = 'dark' | 'light' | 'system'
export interface DevtoolsErrorType {
/**
* The name of the error.
*/
name: string
/**
* How the error is initialized.
*/
initializer: (query: Query) => Error
}
export interface QueryDevtoolsProps {
readonly client: QueryClient
queryFlavor: string
version: string
onlineManager: typeof onlineManager
buttonPosition?: DevtoolsButtonPosition
position?: DevtoolsPosition
initialIsOpen?: boolean
errorTypes?: Array<DevtoolsErrorType>
shadowDOMTarget?: ShadowRoot
onClose?: () => unknown
hideDisabledQueries?: boolean
theme?: Theme
}
export const QueryDevtoolsContext = createContext<QueryDevtoolsProps>({
client: undefined as unknown as QueryClient,
onlineManager: undefined as unknown as typeof onlineManager,
queryFlavor: '',
version: '',
shadowDOMTarget: undefined,
})
export function useQueryDevtoolsContext() {
return useContext(QueryDevtoolsContext)
}
@@ -0,0 +1,10 @@
import { createContext, useContext } from 'solid-js'
import type { Accessor } from 'solid-js'
export const ThemeContext = createContext<Accessor<'light' | 'dark'>>(
() => 'dark' as const,
)
export function useTheme() {
return useContext(ThemeContext)
}
+3
View File
@@ -0,0 +1,3 @@
export * from './PiPContext'
export * from './QueryDevtoolsContext'
export * from './ThemeContext'