forked from defunkt/toes
230 lines
6.9 KiB
TypeScript
230 lines
6.9 KiB
TypeScript
import { theme } from './themes'
|
|
|
|
// Iframe cache - these never get recreated once loaded
|
|
// Use a global to survive hot reloads
|
|
const iframes: Map<string, { iframe: HTMLIFrameElement; port: number; contentHeight?: number }> =
|
|
(window as any).__toolIframes ??= new Map()
|
|
|
|
// Track current state to avoid unnecessary DOM updates
|
|
// Also stored on window to survive hot reloads
|
|
let currentTool: string | null = (window as any).__currentTool ?? null
|
|
|
|
// Get the stable container (outside Hono-managed DOM)
|
|
const getContainer = () => document.getElementById('tool-iframes')
|
|
|
|
// Callback for tool navigation requests
|
|
let onNavigateTool: ((tool: string, params: Record<string, string>) => void) | null = null
|
|
|
|
export function setNavigateToolHandler(handler: (tool: string, params: Record<string, string>) => void) {
|
|
onNavigateTool = handler
|
|
}
|
|
|
|
// Listen for messages from iframes
|
|
function setupMessageListener() {
|
|
if ((window as any).__toolIframeMessageListener) return
|
|
; (window as any).__toolIframeMessageListener = true
|
|
|
|
window.addEventListener('message', (event) => {
|
|
const { type } = event.data || {}
|
|
|
|
if (type === 'resize-iframe') {
|
|
const height = event.data.height
|
|
if (!height || typeof height !== 'number') return
|
|
|
|
// Find which iframe sent this message
|
|
for (const [, cached] of iframes) {
|
|
if (cached.iframe.contentWindow === event.source) {
|
|
cached.contentHeight = height
|
|
cached.iframe.style.height = `100vh`
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type === 'navigate-tool') {
|
|
const { tool, params } = event.data
|
|
if (tool && onNavigateTool) {
|
|
onNavigateTool(tool, params || {})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Initialize the container styles
|
|
export function initToolIframes() {
|
|
const container = getContainer()
|
|
if (!container) return
|
|
|
|
setupMessageListener()
|
|
|
|
// Restore iframe cache from DOM if module was hot-reloaded
|
|
if (iframes.size === 0) {
|
|
const existingIframes = container.querySelectorAll('iframe')
|
|
existingIframes.forEach(iframe => {
|
|
const match = iframe.src.match(/localhost:(\d+)/)
|
|
if (match && match[1]) {
|
|
const port = parseInt(match[1], 10)
|
|
const toolName = iframe.dataset.toolName
|
|
const appName = iframe.dataset.appName
|
|
if (toolName) {
|
|
const cacheKey = appName ? `${toolName}:${appName}` : toolName
|
|
iframes.set(cacheKey, { iframe, port })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
container.style.cssText = `
|
|
display: none;
|
|
position: fixed;
|
|
background: ${theme('colors-bg')};
|
|
overflow: auto;
|
|
`
|
|
}
|
|
|
|
// Build URL with params
|
|
function buildToolUrl(port: number, params: Record<string, string>): string {
|
|
const searchParams = new URLSearchParams(params)
|
|
const query = searchParams.toString()
|
|
return query ? `http://localhost:${port}?${query}` : `http://localhost:${port}`
|
|
}
|
|
|
|
// Build cache key from tool name and params
|
|
function buildCacheKey(toolName: string, params: Record<string, string>): string {
|
|
const parts = [toolName]
|
|
const sortedKeys = Object.keys(params).sort()
|
|
for (const key of sortedKeys) {
|
|
parts.push(`${key}=${params[key]}`)
|
|
}
|
|
return parts.join(':')
|
|
}
|
|
|
|
// Navigate to a tool with specific params (called from postMessage handler)
|
|
export function navigateToTool(
|
|
toolName: string,
|
|
params: Record<string, string>,
|
|
tools: Array<{ name: string; port?: number; state: string }>,
|
|
onSelectTab: (tab: string) => void
|
|
) {
|
|
const tool = tools.find(t => t.name === toolName)
|
|
if (!tool || tool.state !== 'running' || !tool.port) return
|
|
|
|
const container = getContainer()
|
|
if (!container) return
|
|
|
|
const cacheKey = buildCacheKey(toolName, params)
|
|
|
|
// Create iframe if needed
|
|
let cached = iframes.get(cacheKey)
|
|
if (!cached || cached.port !== tool.port) {
|
|
const iframe = document.createElement('iframe')
|
|
iframe.src = buildToolUrl(tool.port, params)
|
|
iframe.dataset.toolName = toolName
|
|
iframe.dataset.appName = params.app ?? ''
|
|
iframe.style.cssText = `width: 100%; border: none;`
|
|
cached = { iframe, port: tool.port }
|
|
iframes.set(cacheKey, cached)
|
|
container.appendChild(iframe)
|
|
}
|
|
|
|
// Switch to that tab
|
|
onSelectTab(toolName)
|
|
|
|
// Force show this specific iframe
|
|
currentTool = cacheKey
|
|
;(window as any).__currentTool = cacheKey
|
|
for (const [key, { iframe }] of iframes) {
|
|
iframe.style.display = key === cacheKey ? 'block' : 'none'
|
|
}
|
|
}
|
|
|
|
// Update which iframe is visible based on selected tab and tool state
|
|
export function updateToolIframes(
|
|
selectedTab: string,
|
|
tools: Array<{ name: string; port?: number; state: string }>,
|
|
selectedApp: string | null
|
|
) {
|
|
const container = getContainer()
|
|
if (!container) return
|
|
|
|
// Find the selected tool
|
|
const selectedTool = tools.find(t => t.name === selectedTab)
|
|
const showIframe = selectedTool?.state === 'running' && selectedTool?.port
|
|
|
|
if (!showIframe) {
|
|
if (currentTool !== null) {
|
|
container.style.display = 'none'
|
|
currentTool = null
|
|
;(window as any).__currentTool = null
|
|
}
|
|
return
|
|
}
|
|
|
|
const tool = selectedTool!
|
|
|
|
// Build params and cache key
|
|
const params: Record<string, string> = {}
|
|
if (selectedApp) params.app = selectedApp
|
|
const cacheKey = buildCacheKey(tool.name, params)
|
|
|
|
// Skip if nothing changed
|
|
if (currentTool === cacheKey) {
|
|
const tabContent = document.querySelector('[data-tool-target]')
|
|
if (tabContent) {
|
|
const rect = tabContent.getBoundingClientRect()
|
|
container.style.top = `${rect.top}px`
|
|
container.style.left = `${rect.left}px`
|
|
container.style.width = `${rect.width}px`
|
|
container.style.height = '100vh'
|
|
}
|
|
return
|
|
}
|
|
|
|
// Position container over the tab content area
|
|
const tabContent = document.querySelector('[data-tool-target]')
|
|
if (!tabContent) {
|
|
container.style.display = 'none'
|
|
currentTool = null
|
|
return
|
|
}
|
|
|
|
const rect = tabContent.getBoundingClientRect()
|
|
container.style.cssText = `
|
|
display: block;
|
|
position: fixed;
|
|
top: ${rect.top}px;
|
|
left: ${rect.left}px;
|
|
width: ${rect.width}px;
|
|
height: calc(100vh - ${rect.top}px);
|
|
background: ${theme('colors-bg')};
|
|
z-index: 100;
|
|
overflow: auto;
|
|
`
|
|
|
|
// Get or create the iframe
|
|
let cached = iframes.get(cacheKey)
|
|
|
|
if (!cached || cached.port !== tool.port) {
|
|
const iframe = document.createElement('iframe')
|
|
iframe.src = buildToolUrl(tool.port!, params)
|
|
iframe.dataset.toolName = tool.name
|
|
iframe.dataset.appName = selectedApp ?? ''
|
|
iframe.style.cssText = `width: 100%; border: none;`
|
|
cached = { iframe, port: tool.port! }
|
|
iframes.set(cacheKey, cached)
|
|
container.appendChild(iframe)
|
|
}
|
|
|
|
// Show only the selected iframe
|
|
for (const [key, { iframe }] of iframes) {
|
|
const shouldShow = key === cacheKey
|
|
if (shouldShow && iframe.parentElement !== container) {
|
|
container.appendChild(iframe)
|
|
}
|
|
iframe.style.display = shouldShow ? 'block' : 'none'
|
|
}
|
|
|
|
currentTool = cacheKey
|
|
;(window as any).__currentTool = cacheKey
|
|
}
|