fix iframes, maybe
This commit is contained in:
parent
d45144478d
commit
ae38084440
3
apps/clock/20260130-000000/TODO.txt
Normal file
3
apps/clock/20260130-000000/TODO.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# clock TODO
|
||||||
|
[x] do something
|
||||||
|
[ ] do more things
|
||||||
|
|
@ -118,12 +118,11 @@ const CodeBlock = define('CodeBlock', {
|
||||||
margin: '20px 0',
|
margin: '20px 0',
|
||||||
border: `1px solid ${theme('border')}`,
|
border: `1px solid ${theme('border')}`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
overflow: 'auto',
|
overflowX: 'auto',
|
||||||
selectors: {
|
selectors: {
|
||||||
'& pre': {
|
'& pre': {
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: '15px',
|
padding: '15px',
|
||||||
overflow: 'auto',
|
|
||||||
whiteSpace: 'pre',
|
whiteSpace: 'pre',
|
||||||
backgroundColor: theme('codeBg'),
|
backgroundColor: theme('codeBg'),
|
||||||
},
|
},
|
||||||
|
|
@ -185,9 +184,17 @@ const initScript = `
|
||||||
document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||||
});
|
});
|
||||||
function sendHeight() {
|
function sendHeight() {
|
||||||
window.parent.postMessage({ type: 'resize-iframe', height: document.documentElement.scrollHeight }, '*');
|
// Measure the actual content container (skip script tags)
|
||||||
|
var container = document.querySelector('.Container');
|
||||||
|
if (!container) return;
|
||||||
|
var rect = container.getBoundingClientRect();
|
||||||
|
var height = rect.bottom + 20; // Add some padding
|
||||||
|
window.parent.postMessage({ type: 'resize-iframe', height: height }, '*');
|
||||||
}
|
}
|
||||||
sendHeight();
|
sendHeight();
|
||||||
|
setTimeout(sendHeight, 50);
|
||||||
|
setTimeout(sendHeight, 200);
|
||||||
|
setTimeout(sendHeight, 500);
|
||||||
new ResizeObserver(sendHeight).observe(document.body);
|
new ResizeObserver(sendHeight).observe(document.body);
|
||||||
window.addEventListener('load', sendHeight);
|
window.addEventListener('load', sendHeight);
|
||||||
})();
|
})();
|
||||||
|
|
@ -280,6 +287,7 @@ function getLanguage(filename: string): string {
|
||||||
|
|
||||||
app.get('/', async c => {
|
app.get('/', async c => {
|
||||||
const appName = c.req.query('app')
|
const appName = c.req.query('app')
|
||||||
|
const version = c.req.query('version') || 'current'
|
||||||
const filePath = c.req.query('file') || ''
|
const filePath = c.req.query('file') || ''
|
||||||
|
|
||||||
if (!appName) {
|
if (!appName) {
|
||||||
|
|
@ -290,14 +298,15 @@ app.get('/', async c => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const appPath = join(APPS_DIR, appName, 'current')
|
const appPath = join(APPS_DIR, appName, version)
|
||||||
|
const versionSuffix = version !== 'current' ? ` @ ${version}` : ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await stat(appPath)
|
await stat(appPath)
|
||||||
} catch {
|
} catch {
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title="Code Browser">
|
<Layout title="Code Browser">
|
||||||
<ErrorBox>App "{appName}" not found</ErrorBox>
|
<ErrorBox>App "{appName}" (version: {version}) not found</ErrorBox>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -309,21 +318,22 @@ app.get('/', async c => {
|
||||||
fileStats = await stat(fullPath)
|
fileStats = await stat(fullPath)
|
||||||
} catch {
|
} catch {
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title="Code Browser" subtitle={appName}>
|
<Layout title="Code Browser" subtitle={appName + versionSuffix}>
|
||||||
<ErrorBox>Path "{filePath}" not found</ErrorBox>
|
<ErrorBox>Path "{filePath}" not found</ErrorBox>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentPath = filePath.split('/').slice(0, -1).join('/')
|
const parentPath = filePath.split('/').slice(0, -1).join('/')
|
||||||
const backUrl = `/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`
|
const versionParam = version !== 'current' ? `&version=${version}` : ''
|
||||||
|
const backUrl = `/?app=${appName}${versionParam}${parentPath ? `&file=${parentPath}` : ''}`
|
||||||
|
|
||||||
if (fileStats.isFile()) {
|
if (fileStats.isFile()) {
|
||||||
const content = readFileSync(fullPath, 'utf-8')
|
const content = readFileSync(fullPath, 'utf-8')
|
||||||
const language = getLanguage(basename(fullPath))
|
const language = getLanguage(basename(fullPath))
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title={`${appName}/${filePath}`} subtitle={`${appName}/${filePath}`} highlight>
|
<Layout title={`${appName}/${filePath}`} subtitle={`${appName}${versionSuffix}/${filePath}`} highlight>
|
||||||
<BackLink href={backUrl}>← Back</BackLink>
|
<BackLink href={backUrl}>← Back</BackLink>
|
||||||
<CodeBlock>
|
<CodeBlock>
|
||||||
<CodeHeader>{basename(fullPath)}</CodeHeader>
|
<CodeHeader>{basename(fullPath)}</CodeHeader>
|
||||||
|
|
@ -336,12 +346,12 @@ app.get('/', async c => {
|
||||||
const files = await listFiles(appPath, filePath)
|
const files = await listFiles(appPath, filePath)
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title={`${appName}${filePath ? `/${filePath}` : ''}`} subtitle={`${appName}${filePath ? `/${filePath}` : ''}`}>
|
<Layout title={`${appName}${filePath ? `/${filePath}` : ''}`} subtitle={`${appName}${versionSuffix}${filePath ? `/${filePath}` : ''}`}>
|
||||||
{filePath && <BackLink href={backUrl}>← Back</BackLink>}
|
{filePath && <BackLink href={backUrl}>← Back</BackLink>}
|
||||||
<FileList>
|
<FileList>
|
||||||
{files.map(file => (
|
{files.map(file => (
|
||||||
<FileItem>
|
<FileItem>
|
||||||
<FileLink href={`/?app=${appName}&file=${file.path}`}>
|
<FileLink href={`/?app=${appName}${versionParam}&file=${file.path}`}>
|
||||||
{file.isDirectory ? <FolderIcon /> : <FileIconSvg />}
|
{file.isDirectory ? <FolderIcon /> : <FileIconSvg />}
|
||||||
<span>{file.name}</span>
|
<span>{file.name}</span>
|
||||||
</FileLink>
|
</FileLink>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ const VersionLink = define('VersionLink', {
|
||||||
color: theme('link'),
|
color: theme('link'),
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
|
cursor: 'pointer',
|
||||||
states: {
|
states: {
|
||||||
':hover': {
|
':hover': {
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
|
|
@ -230,15 +231,15 @@ app.get('/', async c => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the code tool's port from environment or use a relative path
|
|
||||||
const codeToolUrl = process.env.CODE_TOOL_URL || '/tool/code'
|
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title="Versions" subtitle={appName}>
|
<Layout title="Versions" subtitle={appName}>
|
||||||
<VersionList>
|
<VersionList>
|
||||||
{versions.map(v => (
|
{versions.map(v => (
|
||||||
<VersionItem>
|
<VersionItem>
|
||||||
<VersionLink href={`${codeToolUrl}?app=${appName}&version=${v.name}`} target="_top">
|
<VersionLink
|
||||||
|
href="#"
|
||||||
|
onclick={`window.parent.postMessage({type:'navigate-tool',tool:'code',params:{app:'${appName}',version:'${v.name}'}}, '*'); return false;`}
|
||||||
|
>
|
||||||
{formatTimestamp(v.name)}
|
{formatTimestamp(v.name)}
|
||||||
</VersionLink>
|
</VersionLink>
|
||||||
{v.isCurrent && <Badge>current</Badge>}
|
{v.isCurrent && <Badge>current</Badge>}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { render as renderApp } from 'hono/jsx/dom'
|
import { render as renderApp } from 'hono/jsx/dom'
|
||||||
import { Dashboard } from './components'
|
import { Dashboard } from './components'
|
||||||
import { apps, selectedApp, selectedTab, setApps, setSelectedApp } from './state'
|
import { apps, selectedApp, selectedTab, setApps, setSelectedApp, setSelectedTab } from './state'
|
||||||
import { initModal } from './components/modal'
|
import { initModal } from './components/modal'
|
||||||
import { initToolIframes, updateToolIframes } from './tool-iframes'
|
import { initToolIframes, updateToolIframes, setNavigateToolHandler, navigateToTool } from './tool-iframes'
|
||||||
import { initUpdate } from './update'
|
import { initUpdate } from './update'
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
|
|
@ -19,6 +19,15 @@ initModal(render)
|
||||||
initUpdate(render)
|
initUpdate(render)
|
||||||
initToolIframes()
|
initToolIframes()
|
||||||
|
|
||||||
|
// Handle tool-to-tool navigation via postMessage
|
||||||
|
setNavigateToolHandler((tool, params) => {
|
||||||
|
const tools = apps.filter(a => a.tool)
|
||||||
|
navigateToTool(tool, params, tools, (tab) => {
|
||||||
|
setSelectedTab(tab)
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Set theme based on system preference
|
// Set theme based on system preference
|
||||||
const setTheme = () => {
|
const setTheme = () => {
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { theme } from './themes'
|
||||||
|
|
||||||
// Iframe cache - these never get recreated once loaded
|
// Iframe cache - these never get recreated once loaded
|
||||||
// Use a global to survive hot reloads
|
// Use a global to survive hot reloads
|
||||||
const iframes: Map<string, { iframe: HTMLIFrameElement; port: number }> =
|
const iframes: Map<string, { iframe: HTMLIFrameElement; port: number; contentHeight?: number }> =
|
||||||
(window as any).__toolIframes ??= new Map()
|
(window as any).__toolIframes ??= new Map()
|
||||||
|
|
||||||
// Track current state to avoid unnecessary DOM updates
|
// Track current state to avoid unnecessary DOM updates
|
||||||
|
|
@ -12,11 +12,51 @@ let currentTool: string | null = (window as any).__currentTool ?? null
|
||||||
// Get the stable container (outside Hono-managed DOM)
|
// Get the stable container (outside Hono-managed DOM)
|
||||||
const getContainer = () => document.getElementById('tool-iframes')
|
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
|
// Initialize the container styles
|
||||||
export function initToolIframes() {
|
export function initToolIframes() {
|
||||||
const container = getContainer()
|
const container = getContainer()
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
|
setupMessageListener()
|
||||||
|
|
||||||
// Restore iframe cache from DOM if module was hot-reloaded
|
// Restore iframe cache from DOM if module was hot-reloaded
|
||||||
if (iframes.size === 0) {
|
if (iframes.size === 0) {
|
||||||
const existingIframes = container.querySelectorAll('iframe')
|
const existingIframes = container.querySelectorAll('iframe')
|
||||||
|
|
@ -38,9 +78,66 @@ export function initToolIframes() {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: ${theme('colors-bg')};
|
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
|
// Update which iframe is visible based on selected tab and tool state
|
||||||
export function updateToolIframes(
|
export function updateToolIframes(
|
||||||
selectedTab: string,
|
selectedTab: string,
|
||||||
|
|
@ -55,7 +152,6 @@ export function updateToolIframes(
|
||||||
const showIframe = selectedTool?.state === 'running' && selectedTool?.port
|
const showIframe = selectedTool?.state === 'running' && selectedTool?.port
|
||||||
|
|
||||||
if (!showIframe) {
|
if (!showIframe) {
|
||||||
// Only update if state changed
|
|
||||||
if (currentTool !== null) {
|
if (currentTool !== null) {
|
||||||
container.style.display = 'none'
|
container.style.display = 'none'
|
||||||
currentTool = null
|
currentTool = null
|
||||||
|
|
@ -66,19 +162,20 @@ export function updateToolIframes(
|
||||||
|
|
||||||
const tool = selectedTool!
|
const tool = selectedTool!
|
||||||
|
|
||||||
// Build cache key for tool + app combination
|
// Build params and cache key
|
||||||
const cacheKey = selectedApp ? `${tool.name}:${selectedApp}` : tool.name
|
const params: Record<string, string> = {}
|
||||||
|
if (selectedApp) params.app = selectedApp
|
||||||
|
const cacheKey = buildCacheKey(tool.name, params)
|
||||||
|
|
||||||
// Skip if nothing changed
|
// Skip if nothing changed
|
||||||
if (currentTool === cacheKey) {
|
if (currentTool === cacheKey) {
|
||||||
// Just update position in case of scroll/resize
|
|
||||||
const tabContent = document.querySelector('[data-tool-target]')
|
const tabContent = document.querySelector('[data-tool-target]')
|
||||||
if (tabContent) {
|
if (tabContent) {
|
||||||
const rect = tabContent.getBoundingClientRect()
|
const rect = tabContent.getBoundingClientRect()
|
||||||
container.style.top = `${rect.top}px`
|
container.style.top = `${rect.top}px`
|
||||||
container.style.left = `${rect.left}px`
|
container.style.left = `${rect.left}px`
|
||||||
container.style.width = `${rect.width}px`
|
container.style.width = `${rect.width}px`
|
||||||
container.style.height = `${rect.height}px`
|
container.style.height = '100vh'
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -98,35 +195,27 @@ export function updateToolIframes(
|
||||||
top: ${rect.top}px;
|
top: ${rect.top}px;
|
||||||
left: ${rect.left}px;
|
left: ${rect.left}px;
|
||||||
width: ${rect.width}px;
|
width: ${rect.width}px;
|
||||||
height: ${rect.height}px;
|
height: calc(100vh - ${rect.top}px);
|
||||||
background: ${theme('colors-bg')};
|
background: ${theme('colors-bg')};
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
overflow: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
// Get or create the iframe for this tool + app combination
|
// Get or create the iframe
|
||||||
let cached = iframes.get(cacheKey)
|
let cached = iframes.get(cacheKey)
|
||||||
|
|
||||||
if (!cached || cached.port !== tool.port) {
|
if (!cached || cached.port !== tool.port) {
|
||||||
// Create new iframe (first time or port changed)
|
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe')
|
||||||
const url = selectedApp
|
iframe.src = buildToolUrl(tool.port!, params)
|
||||||
? `http://localhost:${tool.port}?app=${encodeURIComponent(selectedApp)}`
|
iframe.dataset.toolName = tool.name
|
||||||
: `http://localhost:${tool.port}`
|
iframe.dataset.appName = selectedApp ?? ''
|
||||||
iframe.src = url
|
iframe.style.cssText = `width: 100%; border: none;`
|
||||||
iframe.dataset.toolName = tool.name // For hot reload recovery
|
|
||||||
iframe.dataset.appName = selectedApp ?? '' // For hot reload recovery
|
|
||||||
iframe.style.cssText = `
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
`
|
|
||||||
cached = { iframe, port: tool.port! }
|
cached = { iframe, port: tool.port! }
|
||||||
iframes.set(cacheKey, cached)
|
iframes.set(cacheKey, cached)
|
||||||
// Add to container
|
|
||||||
container.appendChild(iframe)
|
container.appendChild(iframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show only the selected iframe, hide others
|
// Show only the selected iframe
|
||||||
for (const [key, { iframe }] of iframes) {
|
for (const [key, { iframe }] of iframes) {
|
||||||
const shouldShow = key === cacheKey
|
const shouldShow = key === cacheKey
|
||||||
if (shouldShow && iframe.parentElement !== container) {
|
if (shouldShow && iframe.parentElement !== container) {
|
||||||
|
|
|
||||||
|
|
@ -640,10 +640,15 @@ function watchAppsDir() {
|
||||||
const { pkg, error } = loadApp(dir)
|
const { pkg, error } = loadApp(dir)
|
||||||
|
|
||||||
// Update icon, tool, and error from package.json
|
// Update icon, tool, and error from package.json
|
||||||
|
const iconChanged = app.icon !== pkg.toes?.icon
|
||||||
|
const toolChanged = app.tool !== pkg.toes?.tool
|
||||||
app.icon = pkg.toes?.icon
|
app.icon = pkg.toes?.icon
|
||||||
app.tool = pkg.toes?.tool
|
app.tool = pkg.toes?.tool
|
||||||
app.error = error
|
app.error = error
|
||||||
|
|
||||||
|
// Broadcast if icon or tool changed
|
||||||
|
if (iconChanged || toolChanged) update()
|
||||||
|
|
||||||
// App became valid - start it if stopped
|
// App became valid - start it if stopped
|
||||||
if (!error && app.state === 'invalid') {
|
if (!error && app.state === 'invalid') {
|
||||||
app.state = 'stopped'
|
app.state = 'stopped'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user