@gigabuddy/gadgets 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../libs/gadgets/src/lib/createGadgetRenderer.ts", "../../../libs/gadgets/src/lib/setupGadgetBreakout.ts"],
4
- "sourcesContent": ["/**\n * Creates a self-contained HTML string for rendering a gadget component in an iframe.\n *\n * Loads React 18, ReactDOM, Babel (for JSX transpilation), and Tailwind CSS from CDNs.\n * Sets up the full gadget bridge: state, breakout, actions, chat, context, and hot-swap.\n *\n * Usage:\n * import { createGadgetRenderer, setupGadgetBreakout } from '@gigabuddy/chat';\n *\n * const html = createGadgetRenderer(componentCode, data, { viewport: 'compact', stateEnabled: true });\n * iframe.srcdoc = html;\n * setupGadgetBreakout(iframe);\n *\n * The gadget component receives these props:\n * function Gadget({ data, viewport, state, userId, breakout, chat, context })\n *\n * Bridge protocol (postMessage):\n * - Host \u2192 iframe: `gadget-set-data` \u2014 push new props\n * - Host \u2192 iframe: `gadget-state-init` / `gadget-state-update` \u2014 state changes\n * - Host \u2192 iframe: `gadget-update-component` \u2014 hot-swap component code\n * - Host \u2192 iframe: `gadget-breakout-started` \u2014 breakout activated with originalRect\n * - iframe \u2192 Host: `gadget-interaction` \u2014 user dispatched an action\n * - iframe \u2192 Host: `gadget-resize` \u2014 auto-height\n * - iframe \u2192 Host: `gadget-request-breakout` / `gadget-exit-breakout` \u2014 breakout lifecycle\n * - iframe \u2192 Host: `gadget-action-request` / `gadget-chat-request` / `gadget-context-request` \u2014 API calls\n */\nexport function createGadgetRenderer(\n componentCode: string,\n data: unknown = {},\n options?: {\n viewport?: 'compact' | 'full' | 'mobile';\n stateEnabled?: boolean;\n chatEnabled?: boolean;\n contextEnabled?: boolean;\n assets?: Record<string, { url: string; name: string }>;\n },\n): string {\n const serializedData = JSON.stringify(data);\n const viewport = options?.viewport ?? 'full';\n const stateEnabled = options?.stateEnabled ?? false;\n const chatEnabled = options?.chatEnabled ?? false;\n const contextEnabled = options?.contextEnabled ?? false;\n const escapedCode = componentCode.replace(/<\\/script>/g, '<\\\\/script>');\n const serializedAssets = JSON.stringify(options?.assets ?? {});\n\n const stateBridgeScript = stateEnabled\n ? `\n window.gadget.state = {\n shared: {},\n user: {},\n userId: null,\n _sharedCallbacks: [],\n _userCallbacks: [],\n\n dispatch: function(action, data) {\n window.parent.postMessage({ type: 'gadget-interaction', action: action, data: data || {} }, '*');\n },\n\n onSharedChange: function(callback) {\n window.gadget.state._sharedCallbacks.push(callback);\n callback(window.gadget.state.shared);\n return function() {\n var i = window.gadget.state._sharedCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.state._sharedCallbacks.splice(i, 1);\n };\n },\n\n onUserChange: function(callback) {\n window.gadget.state._userCallbacks.push(callback);\n callback(window.gadget.state.user);\n return function() {\n var i = window.gadget.state._userCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.state._userCallbacks.splice(i, 1);\n };\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && (d.type === 'gadget-state-init' || d.type === 'gadget-state-update')) {\n if (d.userId !== undefined) {\n window.gadget.state.userId = d.userId;\n }\n if (d.shared !== undefined) {\n window.gadget.state.shared = d.shared;\n window.gadget.state._sharedCallbacks.forEach(function(cb) { try { cb(d.shared); } catch(e) {} });\n }\n if (d.user !== undefined) {\n window.gadget.state.user = d.user;\n window.gadget.state._userCallbacks.forEach(function(cb) { try { cb(d.user); } catch(e) {} });\n }\n window.__rerenderGadget();\n }\n });`\n : '';\n\n const chatBridgeScript = chatEnabled\n ? `\n window.gadget.chat = {\n _request: function(method, params) {\n var requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return new Promise(function(resolve, reject) {\n var handler = function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-chat-response' && d.requestId === requestId) {\n window.removeEventListener('message', handler);\n if (d.error) reject(new Error(d.error));\n else resolve(d.result);\n }\n };\n window.addEventListener('message', handler);\n setTimeout(function() { window.removeEventListener('message', handler); reject(new Error('Timed out')); }, 30000);\n window.parent.postMessage({ type: 'gadget-chat-request', requestId: requestId, method: method, params: params || {} }, '*');\n });\n },\n\n _eventCallbacks: [],\n\n createChannel: function(opts) { return window.gadget.chat._request('createChannel', opts); },\n findOrCreateChannel: function(name, opts) { return window.gadget.chat._request('findOrCreateChannel', { name: name, description: opts && opts.description }); },\n listChannels: function() { return window.gadget.chat._request('listChannels'); },\n joinChannel: function(conversationId) { return window.gadget.chat._request('joinChannel', { conversationId: conversationId }); },\n sendMessage: function(conversationId, text) { return window.gadget.chat._request('sendMessage', { conversationId: conversationId, text: text }); },\n getMessages: function(conversationId, opts) { return window.gadget.chat._request('getMessages', { conversationId: conversationId, limit: opts && opts.limit, before: opts && opts.before }); },\n updateChannel: function(conversationId, updates) { return window.gadget.chat._request('updateChannel', { conversationId: conversationId, description: updates && updates.description }); },\n createInvite: function(conversationId) { return window.gadget.chat._request('createInvite', { conversationId: conversationId }); },\n redeemInvite: function(token) { return window.gadget.chat._request('redeemInvite', { token: token }); },\n\n onEvent: function(callback) {\n window.gadget.chat._eventCallbacks.push(callback);\n return function() {\n var i = window.gadget.chat._eventCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.chat._eventCallbacks.splice(i, 1);\n };\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-chat-event') {\n window.gadget.chat._eventCallbacks.forEach(function(cb) { try { cb(d.event); } catch(e) {} });\n window.__rerenderGadget();\n }\n });`\n : '';\n\n const contextBridgeScript = contextEnabled\n ? `\n window.gadget.context = {\n _request: function(method, params) {\n var requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return new Promise(function(resolve, reject) {\n var handler = function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-context-response' && d.id === requestId) {\n window.removeEventListener('message', handler);\n if (d.error) reject(new Error(d.error));\n else resolve(d.result);\n }\n };\n window.addEventListener('message', handler);\n setTimeout(function() { window.removeEventListener('message', handler); reject(new Error('Timed out')); }, 30000);\n window.parent.postMessage({ type: 'gadget-context-request', id: requestId, method: method, params: params || {} }, '*');\n });\n },\n\n _eventCallbacks: [],\n\n listTypes: function() { return window.gadget.context._request('listTypes', {}); },\n getType: function(contextType) { return window.gadget.context._request('getType', { contextType: contextType }); },\n listInstances: function(params) { return window.gadget.context._request('listInstances', params || {}); },\n getInstance: function(id) { return window.gadget.context._request('getInstance', { id: id }); },\n createInstance: function(params) { return window.gadget.context._request('createInstance', params); },\n updateInstance: function(id, data) { return window.gadget.context._request('updateInstance', { id: id, data: data }); },\n deleteInstance: function(id) { return window.gadget.context._request('deleteInstance', { id: id }); },\n searchKnowledge: function(params) { return window.gadget.context._request('searchKnowledge', params); },\n getEnriched: function(id) { return window.gadget.context._request('getEnriched', { id: id }); },\n getRelated: function(id) { return window.gadget.context._request('getRelated', { id: id }); },\n\n onEvent: function(callback) {\n window.gadget.context._eventCallbacks.push(callback);\n return function() {\n var i = window.gadget.context._eventCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.context._eventCallbacks.splice(i, 1);\n };\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-context-event') {\n window.gadget.context._eventCallbacks.forEach(function(cb) { try { cb(d.event); } catch(e) {} });\n window.__rerenderGadget();\n }\n });`\n : '';\n\n // When state is enabled, pass state prop alongside data and viewport\n const chatPropFragment = chatEnabled ? ', chat: window.gadget.chat' : '';\n const contextPropFragment = contextEnabled ? ', context: window.gadget.context' : '';\n const breakoutPropFragment =\n ', breakout: { active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }';\n const componentProps = stateEnabled\n ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }`\n : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }`;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <script src=\"https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js\"></script>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { background: transparent; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100%; }\n #root { min-height: 100vh; }\n .gadget-error {\n padding: 16px;\n background: #fef2f2;\n border: 1px solid #fecaca;\n border-radius: 8px;\n color: #991b1b;\n font-family: monospace;\n font-size: 13px;\n white-space: pre-wrap;\n word-break: break-word;\n }\n .gadget-error-title {\n font-weight: 600;\n margin-bottom: 8px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n </style>\n</head>\n<body>\n <div id=\"root\"></div>\n <script>\n window.__GADGET_DATA__ = ${serializedData};\n window.__GADGET_VIEWPORT__ = '${viewport}';\n\n window.gadget = {\n data: window.__GADGET_DATA__,\n assets: ${serializedAssets},\n\n breakout: {\n _active: false,\n _originalRect: null,\n request: function() {\n if (window.gadget.breakout._active) return;\n window.gadget.breakout._active = true;\n window.parent.postMessage({ type: 'gadget-request-breakout' }, '*');\n window.__rerenderGadget && window.__rerenderGadget();\n },\n exit: function() {\n if (!window.gadget.breakout._active) return;\n window.gadget.breakout._active = false;\n window.gadget.breakout._originalRect = null;\n window.parent.postMessage({ type: 'gadget-exit-breakout' }, '*');\n window.__rerenderGadget();\n },\n },\n\n callAction: function(input) {\n var requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return new Promise(function(resolve, reject) {\n var handler = function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-action-response' && d.requestId === requestId) {\n window.removeEventListener('message', handler);\n if (d.error) reject(new Error(d.error));\n else resolve(d.result);\n }\n };\n window.addEventListener('message', handler);\n setTimeout(function() {\n window.removeEventListener('message', handler);\n reject(new Error('Action timed out'));\n }, 30000);\n window.parent.postMessage({ type: 'gadget-action-request', requestId: requestId, input: input }, '*');\n });\n },\n };\n${stateBridgeScript}\n${chatBridgeScript}\n${contextBridgeScript}\n\n window.__gadgetRoot = null;\n window.__gadgetComponent = null;\n\n window.__rerenderGadget = function() {\n if (window.__gadgetRoot && window.__gadgetComponent) {\n var C = window.__gadgetComponent;\n var EB = window.__gadgetErrorBoundary;\n window.__gadgetRoot.render(\n React.createElement(EB, null, React.createElement(C, ${componentProps}))\n );\n }\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-set-data') {\n window.__GADGET_DATA__ = d.data;\n window.gadget.data = d.data;\n window.__rerenderGadget();\n }\n if (d && d.type === 'gadget-breakout-started') {\n var wasActive = window.gadget.breakout._active;\n window.gadget.breakout._active = true;\n window.gadget.breakout._originalRect = d.originalRect || null;\n if (!wasActive) window.__rerenderGadget();\n }\n if (d && d.type === 'gadget-update-component' && d.code) {\n try {\n var compiled = Babel.transform(d.code, { presets: ['react'] }).code;\n var fn = new Function('React', 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef',\n compiled + '\\\\nreturn typeof Gadget !== \"undefined\" ? Gadget : null;');\n var NewComponent = fn(React, React.useState, React.useEffect, React.useCallback, React.useMemo, React.useRef);\n if (NewComponent) {\n window.__gadgetComponent = NewComponent;\n if (window.__gadgetErrorBoundaryInstance) {\n window.__gadgetErrorBoundaryInstance.setState({ error: null });\n }\n window.__rerenderGadget();\n }\n } catch (err) {\n console.error('[gadget-hmr] Component update failed:', err);\n }\n }\n });\n </script>\n <script type=\"text/babel\" data-type=\"module\">\n const { useState, useEffect, useCallback, useMemo, useRef } = React;\n\n class ErrorBoundary extends React.Component {\n constructor(props) {\n super(props);\n this.state = { error: null };\n window.__gadgetErrorBoundaryInstance = this;\n }\n static getDerivedStateFromError(error) {\n return { error };\n }\n render() {\n if (this.state.error) {\n return React.createElement('div', { className: 'gadget-error' },\n React.createElement('div', { className: 'gadget-error-title' }, 'Runtime Error'),\n this.state.error.message\n );\n }\n return this.props.children;\n }\n }\n window.__gadgetErrorBoundary = ErrorBoundary;\n\n try {\n ${escapedCode}\n\n const ComponentToRender = typeof Gadget !== 'undefined' ? Gadget : null;\n\n if (ComponentToRender) {\n window.__gadgetComponent = ComponentToRender;\n const root = ReactDOM.createRoot(document.getElementById('root'));\n window.__gadgetRoot = root;\n root.render(\n <ErrorBoundary>\n <ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? ' state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}' : ''}${chatEnabled ? ' chat={window.gadget.chat}' : ''}${contextEnabled ? ' context={window.gadget.context}' : ''} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />\n </ErrorBoundary>\n );\n } else {\n document.getElementById('root').innerHTML =\n '<div class=\"gadget-error\"><div class=\"gadget-error-title\">No component found</div>Define a function called Gadget.</div>';\n }\n } catch (err) {\n document.getElementById('root').innerHTML =\n '<div class=\"gadget-error\"><div class=\"gadget-error-title\">Error</div>' +\n err.message.replace(/</g, '&lt;').replace(/>/g, '&gt;') +\n '</div>';\n }\n\n const rootEl = document.getElementById('root');\n const observer = new ResizeObserver(() => {\n window.parent.postMessage({\n type: 'gadget-resize',\n height: rootEl.scrollHeight,\n }, '*');\n });\n observer.observe(rootEl);\n </script>\n</body>\n</html>`;\n}\n", "/**\n * Enables the gadget bridge protocols on an iframe.\n *\n * **Breakout protocol:** When a gadget calls `breakout.request()`, this handler\n * promotes the iframe to a fullscreen transparent overlay so the gadget can\n * render across the entire viewport (e.g. dice rolls, confetti). When the\n * gadget calls `breakout.exit()`, the iframe is restored.\n *\n * **File URL resolution:** When `resolveFileUrl` is provided, gadgets can request\n * fresh signed URLs for internal file references via `gadget-file-url-request`.\n * This solves the expired signed URL problem for images and files.\n *\n * Usage:\n * import { setupGadgetBreakout } from '@gigabuddy/gadgets';\n * const cleanup = setupGadgetBreakout(iframeElement);\n *\n * // With file URL resolution:\n * const cleanup = setupGadgetBreakout(iframeElement, {\n * resolveFileUrl: async (fileId) => {\n * const res = await fetch(`/api/get-file-url`, { ... });\n * return (await res.json()).url;\n * },\n * });\n */\nexport interface GadgetBridgeOptions {\n resolveFileUrl: (fileId: string) => Promise<string>;\n}\n\nexport function setupGadgetBreakout(iframe: HTMLIFrameElement, options?: GadgetBridgeOptions): () => void {\n const savedStyles: Record<string, string> = {};\n const STYLE_KEYS = [\n 'position',\n 'top',\n 'left',\n 'width',\n 'height',\n 'zIndex',\n 'background',\n 'border',\n 'borderRadius',\n 'maxWidth',\n 'maxHeight',\n 'margin',\n 'transform',\n 'pointerEvents',\n 'overflow',\n 'visibility',\n ] as const;\n\n // Track ancestor overrides so we can restore them\n const ancestorOverrides: { el: HTMLElement; key: string; original: string }[] = [];\n let placeholderDiv: HTMLDivElement | null = null;\n\n function clearAncestorClipping(): void {\n let el = iframe.parentElement;\n while (el && el !== document.body && el !== document.documentElement) {\n const computed = getComputedStyle(el);\n const overflow = computed.overflow + computed.overflowX + computed.overflowY;\n const hasClipping = /hidden|auto|scroll|clip/.test(overflow);\n const hasContainingBlock =\n computed.transform !== 'none' ||\n computed.willChange === 'transform' ||\n computed.contain !== 'none' ||\n computed.filter !== 'none';\n\n if (hasClipping || hasContainingBlock) {\n if (hasClipping) {\n ancestorOverrides.push({ el, key: 'overflow', original: el.style.overflow });\n ancestorOverrides.push({ el, key: 'overflowX', original: el.style.overflowX });\n ancestorOverrides.push({ el, key: 'overflowY', original: el.style.overflowY });\n el.style.overflow = 'visible';\n el.style.overflowX = 'visible';\n el.style.overflowY = 'visible';\n }\n if (hasContainingBlock && computed.transform !== 'none') {\n ancestorOverrides.push({ el, key: 'transform', original: el.style.transform });\n el.style.transform = 'none';\n }\n if (hasContainingBlock && computed.contain !== 'none') {\n ancestorOverrides.push({ el, key: 'contain', original: el.style.contain });\n el.style.contain = 'none';\n }\n }\n el = el.parentElement;\n }\n }\n\n function restoreAncestorClipping(): void {\n for (const { el, key, original } of ancestorOverrides) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (el.style as any)[key] = original;\n }\n ancestorOverrides.length = 0;\n }\n\n function handler(event: MessageEvent): void {\n if (event.source !== iframe.contentWindow) return;\n const d = event.data;\n if (!d || typeof d !== 'object') return;\n\n if (d.type === 'gadget-request-breakout') {\n const rect = iframe.getBoundingClientRect();\n\n // Save current inline styles and attributes\n for (const key of STYLE_KEYS) {\n savedStyles[key] = iframe.style[key as keyof CSSStyleDeclaration] as string;\n }\n savedStyles['_scrolling'] = iframe.getAttribute('scrolling') ?? '';\n\n // Insert a placeholder to preserve layout space while iframe is position:fixed\n placeholderDiv = document.createElement('div');\n placeholderDiv.style.width = `${rect.width}px`;\n placeholderDiv.style.height = `${rect.height}px`;\n placeholderDiv.style.flexShrink = '0';\n iframe.parentElement?.insertBefore(placeholderDiv, iframe);\n\n // Remove scrolling restriction during breakout\n iframe.removeAttribute('scrolling');\n\n // Neutralize ancestor clipping so position:fixed works viewport-wide\n clearAncestorClipping();\n\n // Promote to fullscreen overlay (hidden initially to avoid single-frame flicker\n // while gadget repositions its content based on originalRect)\n Object.assign(iframe.style, {\n position: 'fixed',\n top: '0',\n left: '0',\n width: '100vw',\n height: '100vh',\n zIndex: '99999',\n background: 'transparent',\n border: 'none',\n borderRadius: '0',\n maxWidth: 'none',\n maxHeight: 'none',\n margin: '0',\n transform: 'none',\n pointerEvents: 'none',\n overflow: 'visible',\n visibility: 'hidden',\n });\n\n // Tell the gadget its original position for seamless visual continuity\n iframe.contentWindow?.postMessage(\n {\n type: 'gadget-breakout-started',\n originalRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },\n },\n '*',\n );\n\n // Reveal after gadget has repositioned (double-rAF ensures both the\n // style application and the gadget's postMessage handler have run)\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n iframe.style.visibility = 'visible';\n }),\n );\n }\n\n if (d.type === 'gadget-file-url-request' && options?.resolveFileUrl) {\n const requestId = d.requestId as string;\n const fileId = d.fileId as string;\n if (!requestId || !fileId) return;\n\n void (async () => {\n try {\n const url = await options.resolveFileUrl!(fileId);\n iframe.contentWindow?.postMessage({ type: 'gadget-file-url-response', requestId, url }, '*');\n } catch (err) {\n iframe.contentWindow?.postMessage(\n {\n type: 'gadget-file-url-response',\n requestId,\n error: err instanceof Error ? err.message : 'Failed to resolve file URL',\n },\n '*',\n );\n }\n })();\n return;\n }\n\n if (d.type === 'gadget-exit-breakout') {\n // Remove placeholder\n if (placeholderDiv) {\n placeholderDiv.remove();\n placeholderDiv = null;\n }\n\n // Restore iframe styles\n for (const key of STYLE_KEYS) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (iframe.style as any)[key] = savedStyles[key] ?? '';\n }\n if (savedStyles['_scrolling']) {\n iframe.setAttribute('scrolling', savedStyles['_scrolling']);\n }\n\n // Restore ancestor clipping\n restoreAncestorClipping();\n }\n }\n\n window.addEventListener('message', handler);\n return () => {\n if (placeholderDiv) {\n placeholderDiv.remove();\n placeholderDiv = null;\n }\n restoreAncestorClipping();\n window.removeEventListener('message', handler);\n };\n}\n"],
5
- "mappings": "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",
3
+ "sources": ["../../../libs/gadgets/src/lib/createGadgetRenderer.ts", "../../../libs/gadgets/src/lib/setupGadgetBreakout.ts", "../../../libs/gadgets/src/lib/setupGadgetAwareness.ts", "../../../libs/gadgets/src/lib/sceneGraph.ts"],
4
+ "sourcesContent": ["/**\n * Creates a self-contained HTML string for rendering a gadget component in an iframe.\n *\n * Loads React 18, ReactDOM, Babel (for JSX transpilation), and Tailwind CSS from CDNs.\n * Sets up the full gadget bridge: state, breakout, actions, chat, context, and hot-swap.\n *\n * Usage:\n * import { createGadgetRenderer, setupGadgetBreakout } from '@gigabuddy/chat';\n *\n * const html = createGadgetRenderer(componentCode, data, { viewport: 'compact', stateEnabled: true });\n * iframe.srcdoc = html;\n * setupGadgetBreakout(iframe);\n *\n * The gadget component receives these props:\n * function Gadget({ data, viewport, state, userId, breakout, chat, context })\n *\n * Bridge protocol (postMessage):\n * - Host \u2192 iframe: `gadget-set-data` \u2014 push new props\n * - Host \u2192 iframe: `gadget-state-init` / `gadget-state-update` \u2014 state changes\n * - Host \u2192 iframe: `gadget-update-component` \u2014 hot-swap component code\n * - Host \u2192 iframe: `gadget-breakout-started` \u2014 breakout activated with originalRect\n * - iframe \u2192 Host: `gadget-interaction` \u2014 user dispatched an action\n * - iframe \u2192 Host: `gadget-resize` \u2014 auto-height\n * - iframe \u2192 Host: `gadget-request-breakout` / `gadget-exit-breakout` \u2014 breakout lifecycle\n * - iframe \u2192 Host: `gadget-action-request` / `gadget-chat-request` / `gadget-context-request` \u2014 API calls\n */\nexport function createGadgetRenderer(\n componentCode: string,\n data: unknown = {},\n options?: {\n viewport?: 'compact' | 'full' | 'mobile';\n stateEnabled?: boolean;\n chatEnabled?: boolean;\n contextEnabled?: boolean;\n roomEnabled?: boolean;\n assets?: Record<string, { url: string; name: string }>;\n },\n): string {\n const serializedData = JSON.stringify(data);\n const viewport = options?.viewport ?? 'full';\n const stateEnabled = options?.stateEnabled ?? false;\n const chatEnabled = options?.chatEnabled ?? false;\n const contextEnabled = options?.contextEnabled ?? false;\n const roomEnabled = options?.roomEnabled ?? false;\n const escapedCode = componentCode.replace(/<\\/script>/g, '<\\\\/script>');\n const serializedAssets = JSON.stringify(options?.assets ?? {});\n\n const stateBridgeScript = stateEnabled\n ? `\n window.gadget.state = {\n shared: {},\n user: {},\n userId: null,\n _sharedCallbacks: [],\n _userCallbacks: [],\n\n dispatch: function(action, data) {\n window.parent.postMessage({ type: 'gadget-interaction', action: action, data: data || {} }, '*');\n },\n\n onSharedChange: function(callback) {\n window.gadget.state._sharedCallbacks.push(callback);\n callback(window.gadget.state.shared);\n return function() {\n var i = window.gadget.state._sharedCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.state._sharedCallbacks.splice(i, 1);\n };\n },\n\n onUserChange: function(callback) {\n window.gadget.state._userCallbacks.push(callback);\n callback(window.gadget.state.user);\n return function() {\n var i = window.gadget.state._userCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.state._userCallbacks.splice(i, 1);\n };\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && (d.type === 'gadget-state-init' || d.type === 'gadget-state-update')) {\n if (d.userId !== undefined) {\n window.gadget.state.userId = d.userId;\n }\n if (d.shared !== undefined) {\n window.gadget.state.shared = d.shared;\n window.gadget.state._sharedCallbacks.forEach(function(cb) { try { cb(d.shared); } catch(e) {} });\n }\n if (d.user !== undefined) {\n window.gadget.state.user = d.user;\n window.gadget.state._userCallbacks.forEach(function(cb) { try { cb(d.user); } catch(e) {} });\n }\n window.__rerenderGadget();\n }\n });`\n : '';\n\n const chatBridgeScript = chatEnabled\n ? `\n window.gadget.chat = {\n _request: function(method, params) {\n var requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return new Promise(function(resolve, reject) {\n var handler = function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-chat-response' && d.requestId === requestId) {\n window.removeEventListener('message', handler);\n if (d.error) reject(new Error(d.error));\n else resolve(d.result);\n }\n };\n window.addEventListener('message', handler);\n setTimeout(function() { window.removeEventListener('message', handler); reject(new Error('Timed out')); }, 30000);\n window.parent.postMessage({ type: 'gadget-chat-request', requestId: requestId, method: method, params: params || {} }, '*');\n });\n },\n\n _eventCallbacks: [],\n\n createChannel: function(opts) { return window.gadget.chat._request('createChannel', opts); },\n findOrCreateChannel: function(name, opts) { return window.gadget.chat._request('findOrCreateChannel', { name: name, description: opts && opts.description }); },\n listChannels: function() { return window.gadget.chat._request('listChannels'); },\n joinChannel: function(conversationId) { return window.gadget.chat._request('joinChannel', { conversationId: conversationId }); },\n sendMessage: function(conversationId, text) { return window.gadget.chat._request('sendMessage', { conversationId: conversationId, text: text }); },\n getMessages: function(conversationId, opts) { return window.gadget.chat._request('getMessages', { conversationId: conversationId, limit: opts && opts.limit, before: opts && opts.before }); },\n updateChannel: function(conversationId, updates) { return window.gadget.chat._request('updateChannel', { conversationId: conversationId, description: updates && updates.description }); },\n createInvite: function(conversationId) { return window.gadget.chat._request('createInvite', { conversationId: conversationId }); },\n redeemInvite: function(token) { return window.gadget.chat._request('redeemInvite', { token: token }); },\n\n onEvent: function(callback) {\n window.gadget.chat._eventCallbacks.push(callback);\n return function() {\n var i = window.gadget.chat._eventCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.chat._eventCallbacks.splice(i, 1);\n };\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-chat-event') {\n window.gadget.chat._eventCallbacks.forEach(function(cb) { try { cb(d.event); } catch(e) {} });\n window.__rerenderGadget();\n }\n });`\n : '';\n\n const contextBridgeScript = contextEnabled\n ? `\n window.gadget.context = {\n _request: function(method, params) {\n var requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return new Promise(function(resolve, reject) {\n var handler = function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-context-response' && d.id === requestId) {\n window.removeEventListener('message', handler);\n if (d.error) reject(new Error(d.error));\n else resolve(d.result);\n }\n };\n window.addEventListener('message', handler);\n setTimeout(function() { window.removeEventListener('message', handler); reject(new Error('Timed out')); }, 30000);\n window.parent.postMessage({ type: 'gadget-context-request', id: requestId, method: method, params: params || {} }, '*');\n });\n },\n\n _eventCallbacks: [],\n\n listTypes: function() { return window.gadget.context._request('listTypes', {}); },\n getType: function(contextType) { return window.gadget.context._request('getType', { contextType: contextType }); },\n listInstances: function(params) { return window.gadget.context._request('listInstances', params || {}); },\n getInstance: function(id) { return window.gadget.context._request('getInstance', { id: id }); },\n createInstance: function(params) { return window.gadget.context._request('createInstance', params); },\n updateInstance: function(id, data) { return window.gadget.context._request('updateInstance', { id: id, data: data }); },\n deleteInstance: function(id) { return window.gadget.context._request('deleteInstance', { id: id }); },\n searchKnowledge: function(params) { return window.gadget.context._request('searchKnowledge', params); },\n getEnriched: function(id) { return window.gadget.context._request('getEnriched', { id: id }); },\n getRelated: function(id) { return window.gadget.context._request('getRelated', { id: id }); },\n\n onEvent: function(callback) {\n window.gadget.context._eventCallbacks.push(callback);\n return function() {\n var i = window.gadget.context._eventCallbacks.indexOf(callback);\n if (i !== -1) window.gadget.context._eventCallbacks.splice(i, 1);\n };\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-context-event') {\n window.gadget.context._eventCallbacks.forEach(function(cb) { try { cb(d.event); } catch(e) {} });\n window.__rerenderGadget();\n }\n });`\n : '';\n\n const roomBridgeScript = roomEnabled\n ? `\n // \u2500\u2500 Room presence \u2500\u2500\n window.gadget.room = {\n peers: [],\n userId: null,\n displayName: null,\n _peersCallbacks: [],\n _playerJoinedCallbacks: [],\n _playerLeftCallbacks: [],\n\n setCursor: function(pos) {\n window.parent.postMessage({ type: 'gadget-presence', cursor: pos }, '*');\n },\n setSelection: function(sel) {\n window.parent.postMessage({ type: 'gadget-presence', selection: sel }, '*');\n },\n onPeersChange: function(cb) {\n window.gadget.room._peersCallbacks.push(cb);\n cb(window.gadget.room.peers);\n return function() {\n var i = window.gadget.room._peersCallbacks.indexOf(cb);\n if (i !== -1) window.gadget.room._peersCallbacks.splice(i, 1);\n };\n },\n onPlayerJoined: function(cb) {\n window.gadget.room._playerJoinedCallbacks.push(cb);\n return function() {\n var i = window.gadget.room._playerJoinedCallbacks.indexOf(cb);\n if (i !== -1) window.gadget.room._playerJoinedCallbacks.splice(i, 1);\n };\n },\n onPlayerLeft: function(cb) {\n window.gadget.room._playerLeftCallbacks.push(cb);\n return function() {\n var i = window.gadget.room._playerLeftCallbacks.indexOf(cb);\n if (i !== -1) window.gadget.room._playerLeftCallbacks.splice(i, 1);\n };\n },\n };\n\n // \u2500\u2500 Room chat \u2500\u2500\n window.gadget.roomChat = {\n messages: [],\n _messageCallbacks: [],\n\n send: function(text) {\n window.parent.postMessage({ type: 'gadget-chat-send', text: text }, '*');\n },\n onMessage: function(cb) {\n window.gadget.roomChat._messageCallbacks.push(cb);\n return function() {\n var i = window.gadget.roomChat._messageCallbacks.indexOf(cb);\n if (i !== -1) window.gadget.roomChat._messageCallbacks.splice(i, 1);\n };\n },\n };\n\n // \u2500\u2500 Gestures \u2500\u2500\n window.gadget.gestures = {\n active: [],\n _spawnedCallbacks: [],\n _dismissedCallbacks: [],\n\n spawn: function(gadgetId, anchor, opts) {\n opts = opts || {};\n window.parent.postMessage({\n type: 'gadget-gesture-send',\n gadgetId: gadgetId,\n anchor: anchor,\n ttl: opts.ttl,\n size: opts.size,\n rotation: opts.rotation,\n }, '*');\n },\n dismiss: function(gestureId) {\n window.parent.postMessage({ type: 'gadget-gesture-dismiss', gestureId: gestureId }, '*');\n },\n reportAnchorRect: function(selector, rect) {\n window.parent.postMessage({ type: 'gadget-anchor-rect', selector: selector, rect: rect }, '*');\n },\n onSpawned: function(cb) {\n window.gadget.gestures._spawnedCallbacks.push(cb);\n return function() {\n var i = window.gadget.gestures._spawnedCallbacks.indexOf(cb);\n if (i !== -1) window.gadget.gestures._spawnedCallbacks.splice(i, 1);\n };\n },\n onDismissed: function(cb) {\n window.gadget.gestures._dismissedCallbacks.push(cb);\n return function() {\n var i = window.gadget.gestures._dismissedCallbacks.indexOf(cb);\n if (i !== -1) window.gadget.gestures._dismissedCallbacks.splice(i, 1);\n };\n },\n };\n\n // \u2500\u2500 Buddy attraction \u2500\u2500\n window.gadget.buddy = {\n attract: function(anchor, interest) {\n window.parent.postMessage({\n type: 'gadget-buddy-attract',\n anchor: anchor,\n interest: interest || 'medium',\n }, '*');\n },\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (!d) return;\n\n // Presence updates\n if (d.type === 'gadget-presence-update') {\n window.gadget.room.peers = d.peers || [];\n window.gadget.room._peersCallbacks.forEach(function(cb) { try { cb(d.peers || []); } catch(e) {} });\n window.__rerenderGadget && window.__rerenderGadget();\n }\n\n // Player join/leave\n if (d.type === 'gadget-player-joined') {\n window.gadget.room._playerJoinedCallbacks.forEach(function(cb) { try { cb(d); } catch(e) {} });\n }\n if (d.type === 'gadget-player-left') {\n window.gadget.room.peers = window.gadget.room.peers.filter(function(p) { return p.userId !== d.userId; });\n window.gadget.room._playerLeftCallbacks.forEach(function(cb) { try { cb(d.userId); } catch(e) {} });\n window.__rerenderGadget && window.__rerenderGadget();\n }\n\n // Chat\n if (d.type === 'gadget-chat-message') {\n var msg = d.message || d;\n window.gadget.roomChat.messages.push(msg);\n if (window.gadget.roomChat.messages.length > 100) window.gadget.roomChat.messages.shift();\n window.gadget.roomChat._messageCallbacks.forEach(function(cb) { try { cb(msg); } catch(e) {} });\n window.__rerenderGadget && window.__rerenderGadget();\n }\n if (d.type === 'gadget-chat-history') {\n window.gadget.roomChat.messages = d.messages || [];\n window.__rerenderGadget && window.__rerenderGadget();\n }\n\n // Gestures\n if (d.type === 'gadget-gesture-spawned') {\n window.gadget.gestures.active.push(d.gesture);\n window.gadget.gestures._spawnedCallbacks.forEach(function(cb) { try { cb(d.gesture); } catch(e) {} });\n window.__rerenderGadget && window.__rerenderGadget();\n }\n if (d.type === 'gadget-gesture-dismissed') {\n window.gadget.gestures.active = window.gadget.gestures.active.filter(function(g) { return g.id !== d.gestureId; });\n window.gadget.gestures._dismissedCallbacks.forEach(function(cb) { try { cb(d.gestureId); } catch(e) {} });\n window.__rerenderGadget && window.__rerenderGadget();\n }\n if (d.type === 'gadget-gesture-sync') {\n window.gadget.gestures.active = d.gestures || [];\n window.__rerenderGadget && window.__rerenderGadget();\n }\n\n // Anchor rect request from host\n if (d.type === 'gadget-request-anchor-rect' && d.selector) {\n try {\n var el = document.querySelector(d.selector);\n if (el) {\n var rect = el.getBoundingClientRect();\n window.parent.postMessage({\n type: 'gadget-anchor-rect',\n selector: d.selector,\n rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },\n }, '*');\n }\n } catch(e) {}\n }\n });`\n : '';\n\n // When state is enabled, pass state prop alongside data and viewport\n const chatPropFragment = chatEnabled ? ', chat: window.gadget.chat' : '';\n const contextPropFragment = contextEnabled ? ', context: window.gadget.context' : '';\n const roomPropFragment = roomEnabled\n ? ', room: window.gadget.room, roomChat: window.gadget.roomChat, gestures: window.gadget.gestures'\n : '';\n const breakoutPropFragment =\n ', breakout: { active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }';\n const componentProps = stateEnabled\n ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }`\n : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }`;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <script src=\"https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js\"></script>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { background: transparent; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100%; }\n #root { min-height: 100vh; }\n .gadget-error {\n padding: 16px;\n background: #fef2f2;\n border: 1px solid #fecaca;\n border-radius: 8px;\n color: #991b1b;\n font-family: monospace;\n font-size: 13px;\n white-space: pre-wrap;\n word-break: break-word;\n }\n .gadget-error-title {\n font-weight: 600;\n margin-bottom: 8px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n </style>\n</head>\n<body>\n <div id=\"root\"></div>\n <script>\n window.__GADGET_DATA__ = ${serializedData};\n window.__GADGET_VIEWPORT__ = '${viewport}';\n\n window.gadget = {\n data: window.__GADGET_DATA__,\n assets: ${serializedAssets},\n\n breakout: {\n _active: false,\n _originalRect: null,\n request: function() {\n if (window.gadget.breakout._active) return;\n window.gadget.breakout._active = true;\n window.parent.postMessage({ type: 'gadget-request-breakout' }, '*');\n window.__rerenderGadget && window.__rerenderGadget();\n },\n exit: function() {\n if (!window.gadget.breakout._active) return;\n window.gadget.breakout._active = false;\n window.gadget.breakout._originalRect = null;\n window.parent.postMessage({ type: 'gadget-exit-breakout' }, '*');\n window.__rerenderGadget();\n },\n },\n\n callAction: function(input) {\n var requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return new Promise(function(resolve, reject) {\n var handler = function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-action-response' && d.requestId === requestId) {\n window.removeEventListener('message', handler);\n if (d.error) reject(new Error(d.error));\n else resolve(d.result);\n }\n };\n window.addEventListener('message', handler);\n setTimeout(function() {\n window.removeEventListener('message', handler);\n reject(new Error('Action timed out'));\n }, 30000);\n window.parent.postMessage({ type: 'gadget-action-request', requestId: requestId, input: input }, '*');\n });\n },\n };\n${stateBridgeScript}\n${chatBridgeScript}\n${contextBridgeScript}\n${roomBridgeScript}\n\n window.__gadgetRoot = null;\n window.__gadgetComponent = null;\n\n window.__rerenderGadget = function() {\n if (window.__gadgetRoot && window.__gadgetComponent) {\n var C = window.__gadgetComponent;\n var EB = window.__gadgetErrorBoundary;\n window.__gadgetRoot.render(\n React.createElement(EB, null, React.createElement(C, ${componentProps}))\n );\n }\n };\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (d && d.type === 'gadget-set-data') {\n window.__GADGET_DATA__ = d.data;\n window.gadget.data = d.data;\n window.__rerenderGadget();\n }\n if (d && d.type === 'gadget-breakout-started') {\n var wasActive = window.gadget.breakout._active;\n window.gadget.breakout._active = true;\n window.gadget.breakout._originalRect = d.originalRect || null;\n if (!wasActive) window.__rerenderGadget();\n }\n if (d && d.type === 'gadget-update-component' && d.code) {\n try {\n var compiled = Babel.transform(d.code, { presets: ['react'] }).code;\n var fn = new Function('React', 'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef',\n compiled + '\\\\nreturn typeof Gadget !== \"undefined\" ? Gadget : null;');\n var NewComponent = fn(React, React.useState, React.useEffect, React.useCallback, React.useMemo, React.useRef);\n if (NewComponent) {\n window.__gadgetComponent = NewComponent;\n if (window.__gadgetErrorBoundaryInstance) {\n window.__gadgetErrorBoundaryInstance.setState({ error: null });\n }\n window.__rerenderGadget();\n }\n } catch (err) {\n console.error('[gadget-hmr] Component update failed:', err);\n }\n }\n });\n </script>\n <script type=\"text/babel\" data-type=\"module\">\n const { useState, useEffect, useCallback, useMemo, useRef } = React;\n\n class ErrorBoundary extends React.Component {\n constructor(props) {\n super(props);\n this.state = { error: null };\n window.__gadgetErrorBoundaryInstance = this;\n }\n static getDerivedStateFromError(error) {\n return { error };\n }\n render() {\n if (this.state.error) {\n return React.createElement('div', { className: 'gadget-error' },\n React.createElement('div', { className: 'gadget-error-title' }, 'Runtime Error'),\n this.state.error.message\n );\n }\n return this.props.children;\n }\n }\n window.__gadgetErrorBoundary = ErrorBoundary;\n\n try {\n ${escapedCode}\n\n const ComponentToRender = typeof Gadget !== 'undefined' ? Gadget : null;\n\n if (ComponentToRender) {\n window.__gadgetComponent = ComponentToRender;\n const root = ReactDOM.createRoot(document.getElementById('root'));\n window.__gadgetRoot = root;\n root.render(\n <ErrorBoundary>\n <ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? ' state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}' : ''}${chatEnabled ? ' chat={window.gadget.chat}' : ''}${contextEnabled ? ' context={window.gadget.context}' : ''}${roomEnabled ? ' room={window.gadget.room} roomChat={window.gadget.roomChat} gestures={window.gadget.gestures}' : ''} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />\n </ErrorBoundary>\n );\n } else {\n document.getElementById('root').innerHTML =\n '<div class=\"gadget-error\"><div class=\"gadget-error-title\">No component found</div>Define a function called Gadget.</div>';\n }\n } catch (err) {\n document.getElementById('root').innerHTML =\n '<div class=\"gadget-error\"><div class=\"gadget-error-title\">Error</div>' +\n err.message.replace(/</g, '&lt;').replace(/>/g, '&gt;') +\n '</div>';\n }\n\n const rootEl = document.getElementById('root');\n const observer = new ResizeObserver(() => {\n window.parent.postMessage({\n type: 'gadget-resize',\n height: rootEl.scrollHeight,\n }, '*');\n });\n observer.observe(rootEl);\n </script>\n</body>\n</html>`;\n}\n", "/**\n * Enables the gadget bridge protocols on an iframe.\n *\n * **Breakout protocol:** When a gadget calls `breakout.request()`, this handler\n * promotes the iframe to a fullscreen transparent overlay so the gadget can\n * render across the entire viewport (e.g. dice rolls, confetti). When the\n * gadget calls `breakout.exit()`, the iframe is restored.\n *\n * **File URL resolution:** When `resolveFileUrl` is provided, gadgets can request\n * fresh signed URLs for internal file references via `gadget-file-url-request`.\n * This solves the expired signed URL problem for images and files.\n *\n * Usage:\n * import { setupGadgetBreakout } from '@gigabuddy/gadgets';\n * const cleanup = setupGadgetBreakout(iframeElement);\n *\n * // With file URL resolution:\n * const cleanup = setupGadgetBreakout(iframeElement, {\n * resolveFileUrl: async (fileId) => {\n * const res = await fetch(`/api/get-file-url`, { ... });\n * return (await res.json()).url;\n * },\n * });\n */\nexport interface GadgetBridgeOptions {\n resolveFileUrl: (fileId: string) => Promise<string>;\n}\n\nexport function setupGadgetBreakout(iframe: HTMLIFrameElement, options?: GadgetBridgeOptions): () => void {\n const savedStyles: Record<string, string> = {};\n const STYLE_KEYS = [\n 'position',\n 'top',\n 'left',\n 'width',\n 'height',\n 'zIndex',\n 'background',\n 'border',\n 'borderRadius',\n 'maxWidth',\n 'maxHeight',\n 'margin',\n 'transform',\n 'pointerEvents',\n 'overflow',\n 'visibility',\n ] as const;\n\n // Track ancestor overrides so we can restore them\n const ancestorOverrides: { el: HTMLElement; key: string; original: string }[] = [];\n let placeholderDiv: HTMLDivElement | null = null;\n\n function clearAncestorClipping(): void {\n let el = iframe.parentElement;\n while (el && el !== document.body && el !== document.documentElement) {\n const computed = getComputedStyle(el);\n const overflow = computed.overflow + computed.overflowX + computed.overflowY;\n const hasClipping = /hidden|auto|scroll|clip/.test(overflow);\n const hasContainingBlock =\n computed.transform !== 'none' ||\n computed.willChange === 'transform' ||\n computed.contain !== 'none' ||\n computed.filter !== 'none';\n\n if (hasClipping || hasContainingBlock) {\n if (hasClipping) {\n ancestorOverrides.push({ el, key: 'overflow', original: el.style.overflow });\n ancestorOverrides.push({ el, key: 'overflowX', original: el.style.overflowX });\n ancestorOverrides.push({ el, key: 'overflowY', original: el.style.overflowY });\n el.style.overflow = 'visible';\n el.style.overflowX = 'visible';\n el.style.overflowY = 'visible';\n }\n if (hasContainingBlock && computed.transform !== 'none') {\n ancestorOverrides.push({ el, key: 'transform', original: el.style.transform });\n el.style.transform = 'none';\n }\n if (hasContainingBlock && computed.contain !== 'none') {\n ancestorOverrides.push({ el, key: 'contain', original: el.style.contain });\n el.style.contain = 'none';\n }\n }\n el = el.parentElement;\n }\n }\n\n function restoreAncestorClipping(): void {\n for (const { el, key, original } of ancestorOverrides) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (el.style as any)[key] = original;\n }\n ancestorOverrides.length = 0;\n }\n\n function handler(event: MessageEvent): void {\n if (event.source !== iframe.contentWindow) return;\n const d = event.data;\n if (!d || typeof d !== 'object') return;\n\n if (d.type === 'gadget-request-breakout') {\n const rect = iframe.getBoundingClientRect();\n\n // Save current inline styles and attributes\n for (const key of STYLE_KEYS) {\n savedStyles[key] = iframe.style[key as keyof CSSStyleDeclaration] as string;\n }\n savedStyles['_scrolling'] = iframe.getAttribute('scrolling') ?? '';\n\n // Insert a placeholder to preserve layout space while iframe is position:fixed\n placeholderDiv = document.createElement('div');\n placeholderDiv.style.width = `${rect.width}px`;\n placeholderDiv.style.height = `${rect.height}px`;\n placeholderDiv.style.flexShrink = '0';\n iframe.parentElement?.insertBefore(placeholderDiv, iframe);\n\n // Remove scrolling restriction during breakout\n iframe.removeAttribute('scrolling');\n\n // Neutralize ancestor clipping so position:fixed works viewport-wide\n clearAncestorClipping();\n\n // Promote to fullscreen overlay (hidden initially to avoid single-frame flicker\n // while gadget repositions its content based on originalRect)\n Object.assign(iframe.style, {\n position: 'fixed',\n top: '0',\n left: '0',\n width: '100vw',\n height: '100vh',\n zIndex: '99999',\n background: 'transparent',\n border: 'none',\n borderRadius: '0',\n maxWidth: 'none',\n maxHeight: 'none',\n margin: '0',\n transform: 'none',\n pointerEvents: 'none',\n overflow: 'visible',\n visibility: 'hidden',\n });\n\n // Tell the gadget its original position for seamless visual continuity\n iframe.contentWindow?.postMessage(\n {\n type: 'gadget-breakout-started',\n originalRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },\n },\n '*',\n );\n\n // Reveal after gadget has repositioned (double-rAF ensures both the\n // style application and the gadget's postMessage handler have run)\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n iframe.style.visibility = 'visible';\n }),\n );\n }\n\n if (d.type === 'gadget-file-url-request' && options?.resolveFileUrl) {\n const requestId = d.requestId as string;\n const fileId = d.fileId as string;\n if (!requestId || !fileId) return;\n\n void (async () => {\n try {\n const url = await options.resolveFileUrl!(fileId);\n iframe.contentWindow?.postMessage({ type: 'gadget-file-url-response', requestId, url }, '*');\n } catch (err) {\n iframe.contentWindow?.postMessage(\n {\n type: 'gadget-file-url-response',\n requestId,\n error: err instanceof Error ? err.message : 'Failed to resolve file URL',\n },\n '*',\n );\n }\n })();\n return;\n }\n\n if (d.type === 'gadget-exit-breakout') {\n // Remove placeholder\n if (placeholderDiv) {\n placeholderDiv.remove();\n placeholderDiv = null;\n }\n\n // Restore iframe styles\n for (const key of STYLE_KEYS) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (iframe.style as any)[key] = savedStyles[key] ?? '';\n }\n if (savedStyles['_scrolling']) {\n iframe.setAttribute('scrolling', savedStyles['_scrolling']);\n }\n\n // Restore ancestor clipping\n restoreAncestorClipping();\n }\n }\n\n window.addEventListener('message', handler);\n return () => {\n if (placeholderDiv) {\n placeholderDiv.remove();\n placeholderDiv = null;\n }\n restoreAncestorClipping();\n window.removeEventListener('message', handler);\n };\n}\n", "/**\n * Host-side awareness bridge for gadget iframes.\n *\n * Listens for awareness messages from gadget iframes (anchor registration,\n * awareness publish, subscription config) and forwards filtered room\n * awareness state back to the gadget.\n *\n * Usage:\n * const cleanup = setupGadgetAwareness(iframe, {\n * gadgetId: 'poker',\n * sceneGraph,\n * getAwarenessState: () => roomAwareness.states,\n * onAwarenessPublish: (gadgetId, state) => { ... },\n * throttleMs: 100,\n * });\n */\n\nimport type { SceneGraph } from './sceneGraph.js';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type AwarenessFilter = 'focused' | 'room' | 'all';\n\nexport interface AwarenessParticipant {\n actorId: string;\n displayName?: string;\n cursor?: { x: number; y: number };\n focus?: string;\n intent?: string;\n [key: string]: unknown;\n}\n\nexport interface GadgetAwarenessState {\n /** Participants matching this gadget's subscription filter */\n participants: AwarenessParticipant[];\n /** Anchors registered by this gadget */\n anchors: Array<{\n id: string;\n bounds: { x: number; y: number; w: number; h: number };\n occupant?: string;\n attention: string[];\n }>;\n /** Participants whose focus target is this gadget */\n focused: AwarenessParticipant[];\n}\n\nexport interface SetupGadgetAwarenessOptions {\n /** Unique ID for this gadget instance */\n gadgetId: string;\n /** Shared scene graph for the room */\n sceneGraph: SceneGraph;\n /** Returns current room awareness states (Map<clientId, state>) */\n getAwarenessStates: () => Map<number, { state: Record<string, unknown> }>;\n /** Called when a gadget publishes its own awareness state */\n onAwarenessPublish?: (gadgetId: string, state: Record<string, unknown>) => void;\n /** Called when the scene graph changes (anchors registered/updated) */\n onSceneGraphChange?: () => void;\n /** Throttle interval for awareness forwarding (default: 100ms) */\n throttleMs?: number;\n}\n\n// =============================================================================\n// Setup\n// =============================================================================\n\nexport function setupGadgetAwareness(iframe: HTMLIFrameElement, options: SetupGadgetAwarenessOptions): () => void {\n const {\n gadgetId,\n sceneGraph,\n getAwarenessStates,\n onAwarenessPublish,\n onSceneGraphChange,\n throttleMs = 100,\n } = options;\n\n let filter: AwarenessFilter = 'focused';\n let throttleTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingUpdate = false;\n\n // \u2500\u2500 Coordinate translation \u2500\u2500\n\n function getIframeBounds(): DOMRect {\n return iframe.getBoundingClientRect();\n }\n\n function roomToGadget(rx: number, ry: number, bounds: DOMRect): { x: number; y: number } | null {\n const gx = (rx - bounds.left) / bounds.width;\n const gy = (ry - bounds.top) / bounds.height;\n // Filter out cursors outside the gadget bounds\n if (gx < 0 || gx > 1 || gy < 0 || gy > 1) return null;\n return { x: gx, y: gy };\n }\n\n function gadgetToRoom(gx: number, gy: number, bounds: DOMRect): { x: number; y: number } {\n return {\n x: gx * bounds.width + bounds.left,\n y: gy * bounds.height + bounds.top,\n };\n }\n\n // \u2500\u2500 Build filtered awareness for this gadget \u2500\u2500\n\n function buildAwarenessState(): GadgetAwarenessState {\n const states = getAwarenessStates();\n const bounds = getIframeBounds();\n const allParticipants: AwarenessParticipant[] = [];\n const focused: AwarenessParticipant[] = [];\n\n for (const [, entry] of states) {\n const s = entry.state;\n if (!s || typeof s !== 'object') continue;\n\n const participant: AwarenessParticipant = {\n actorId: (s['actorId'] as string) ?? (s['displayName'] as string) ?? 'unknown',\n displayName: s['displayName'] as string | undefined,\n intent: s['intent'] as string | undefined,\n focus: s['focus'] as string | undefined,\n };\n\n // Translate cursor from room space to gadget space\n const cursorVal = s['cursor'];\n if (cursorVal && typeof cursorVal === 'object') {\n const rc = cursorVal as { x: number; y: number };\n const gc = roomToGadget(rc.x, rc.y, bounds);\n if (gc) {\n participant.cursor = gc;\n }\n }\n\n allParticipants.push(participant);\n\n // Check if this participant is focused on this gadget\n const focusVal = s['focus'];\n if (focusVal === gadgetId || focusVal === `gadget:${gadgetId}`) {\n focused.push(participant);\n }\n }\n\n // Get this gadget's anchors\n const gadgetAnchors = sceneGraph.getGadgetAnchors(gadgetId).map((a) => ({\n id: a.anchorId,\n bounds: a.bounds,\n occupant: a.occupant,\n attention: a.attention,\n }));\n\n return {\n participants: filter === 'focused' ? focused : allParticipants,\n anchors: gadgetAnchors,\n focused,\n };\n }\n\n // \u2500\u2500 Throttled forwarding \u2500\u2500\n\n function scheduleForward(): void {\n if (throttleTimer) {\n pendingUpdate = true;\n return;\n }\n\n forwardAwareness();\n\n throttleTimer = setTimeout(() => {\n throttleTimer = null;\n if (pendingUpdate) {\n pendingUpdate = false;\n forwardAwareness();\n }\n }, throttleMs);\n }\n\n function forwardAwareness(): void {\n if (!iframe.contentWindow) return;\n const state = buildAwarenessState();\n iframe.contentWindow.postMessage({ type: 'gadget-awareness-state', ...state }, '*');\n }\n\n // \u2500\u2500 Handle messages from gadget iframe \u2500\u2500\n\n function handleMessage(event: MessageEvent): void {\n // Only accept messages from our iframe\n if (event.source !== iframe.contentWindow) return;\n const msg = event.data;\n if (!msg || typeof msg.type !== 'string') return;\n\n switch (msg.type) {\n case 'gadget-anchor-register': {\n const anchors = msg.anchors as Array<{\n id: string;\n bounds: { x: number; y: number; w: number; h: number };\n parent?: string;\n }>;\n if (Array.isArray(anchors)) {\n sceneGraph.registerAnchors(gadgetId, anchors);\n onSceneGraphChange?.();\n }\n break;\n }\n\n case 'gadget-anchor-update': {\n const { id, occupant, attention } = msg as {\n id: string;\n occupant?: string | null;\n attention?: string[];\n };\n if (typeof id === 'string') {\n sceneGraph.updateAnchor(gadgetId, id, { occupant, attention });\n onSceneGraphChange?.();\n }\n break;\n }\n\n case 'gadget-awareness-publish': {\n const state = msg.state as Record<string, unknown>;\n if (state && typeof state === 'object') {\n // Translate any cursor from gadget space to room space\n if (state['cursor'] && typeof state['cursor'] === 'object') {\n const gc = state['cursor'] as { x: number; y: number };\n const bounds = getIframeBounds();\n state['cursor'] = gadgetToRoom(gc.x, gc.y, bounds);\n }\n onAwarenessPublish?.(gadgetId, state);\n }\n break;\n }\n\n case 'gadget-awareness-subscribe': {\n const newFilter = msg.filter as AwarenessFilter;\n if (newFilter === 'focused' || newFilter === 'room' || newFilter === 'all') {\n filter = newFilter;\n // Send immediate update with new filter\n forwardAwareness();\n }\n break;\n }\n }\n }\n\n // \u2500\u2500 Setup \u2500\u2500\n\n window.addEventListener('message', handleMessage);\n\n // Send initial awareness state once iframe is loaded\n const onLoad = () => forwardAwareness();\n iframe.addEventListener('load', onLoad);\n\n // \u2500\u2500 Cleanup \u2500\u2500\n\n return () => {\n window.removeEventListener('message', handleMessage);\n iframe.removeEventListener('load', onLoad);\n if (throttleTimer) clearTimeout(throttleTimer);\n sceneGraph.removeGadget(gadgetId);\n onSceneGraphChange?.();\n };\n}\n", "/**\n * Scene Graph \u2014 spatial anchor registry for gadgets in a room.\n *\n * Tracks named anchors registered by gadgets, their parent/child\n * relationships, occupancy, and attention. Queried by the host to\n * build per-gadget awareness state.\n *\n * Anchors are namespaced by gadget: `{gadgetId}:{anchorId}`.\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AnchorBounds {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\nexport interface Anchor {\n /** Fully qualified ID: `{gadgetId}:{anchorId}` */\n qualifiedId: string;\n /** Raw anchor ID as registered by the gadget */\n anchorId: string;\n /** Gadget that owns this anchor */\n gadgetId: string;\n /** Bounds relative to the owning gadget's coordinate space */\n bounds: AnchorBounds;\n /** Parent anchor qualified ID (for nesting) */\n parent?: string;\n /** Who's currently \"at\" this anchor (e.g., seated at a poker table) */\n occupant?: string;\n /** Who's looking at this anchor */\n attention: string[];\n}\n\nexport interface AnchorNode extends Anchor {\n children: AnchorNode[];\n}\n\n// =============================================================================\n// Scene Graph\n// =============================================================================\n\nexport class SceneGraph {\n private anchors = new Map<string, Anchor>();\n private onChange: (() => void) | null = null;\n\n /**\n * Set a callback for when the scene graph changes.\n */\n setOnChange(cb: (() => void) | null): void {\n this.onChange = cb;\n }\n\n /**\n * Register anchors for a gadget. Replaces any existing anchors\n * from the same gadget with the same IDs.\n */\n registerAnchors(gadgetId: string, anchors: Array<{ id: string; bounds: AnchorBounds; parent?: string }>): void {\n for (const a of anchors) {\n const qualifiedId = `${gadgetId}:${a.id}`;\n const parentQualified = a.parent ? `${gadgetId}:${a.parent}` : undefined;\n\n this.anchors.set(qualifiedId, {\n qualifiedId,\n anchorId: a.id,\n gadgetId,\n bounds: a.bounds,\n parent: parentQualified,\n occupant: this.anchors.get(qualifiedId)?.occupant,\n attention: this.anchors.get(qualifiedId)?.attention ?? [],\n });\n }\n this.onChange?.();\n }\n\n /**\n * Update an anchor's occupancy or attention.\n */\n updateAnchor(gadgetId: string, anchorId: string, update: { occupant?: string | null; attention?: string[] }): void {\n const qualifiedId = `${gadgetId}:${anchorId}`;\n const anchor = this.anchors.get(qualifiedId);\n if (!anchor) return;\n\n if (update.occupant !== undefined) {\n anchor.occupant = update.occupant ?? undefined;\n }\n if (update.attention !== undefined) {\n anchor.attention = update.attention;\n }\n this.onChange?.();\n }\n\n /**\n * Remove all anchors for a gadget (cleanup on iframe removal).\n */\n removeGadget(gadgetId: string): void {\n const toRemove: string[] = [];\n for (const [id, anchor] of this.anchors) {\n if (anchor.gadgetId === gadgetId) {\n toRemove.push(id);\n }\n }\n for (const id of toRemove) {\n this.anchors.delete(id);\n }\n if (toRemove.length > 0) this.onChange?.();\n }\n\n /**\n * Get all anchors as a flat map.\n */\n getAnchors(): Map<string, Anchor> {\n return this.anchors;\n }\n\n /**\n * Get anchors for a specific gadget.\n */\n getGadgetAnchors(gadgetId: string): Anchor[] {\n const result: Anchor[] = [];\n for (const anchor of this.anchors.values()) {\n if (anchor.gadgetId === gadgetId) {\n result.push(anchor);\n }\n }\n return result;\n }\n\n /**\n * Get the anchor tree, optionally rooted at a specific anchor.\n */\n getAnchorTree(rootId?: string): AnchorNode[] {\n const childMap = new Map<string | undefined, Anchor[]>();\n\n for (const anchor of this.anchors.values()) {\n const parentKey = anchor.parent ?? undefined;\n if (!childMap.has(parentKey)) childMap.set(parentKey, []);\n childMap.get(parentKey)!.push(anchor);\n }\n\n const buildNode = (anchor: Anchor): AnchorNode => {\n const children = (childMap.get(anchor.qualifiedId) ?? []).map(buildNode);\n return { ...anchor, children };\n };\n\n if (rootId) {\n const root = this.anchors.get(rootId);\n if (!root) return [];\n return [buildNode(root)];\n }\n\n // Return all root-level anchors (no parent)\n return (childMap.get(undefined) ?? []).map(buildNode);\n }\n\n /**\n * Find the anchor at a given point (in room coordinates).\n * Returns the deepest (most specific) anchor containing the point.\n */\n findAnchorAt(x: number, y: number): Anchor | null {\n let best: Anchor | null = null;\n let bestArea = Infinity;\n\n for (const anchor of this.anchors.values()) {\n const b = anchor.bounds;\n if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {\n const area = b.w * b.h;\n if (area < bestArea) {\n best = anchor;\n bestArea = area;\n }\n }\n }\n return best;\n }\n\n /**\n * Get a serializable snapshot of all anchors.\n */\n toJSON(): Array<{\n id: string;\n gadgetId: string;\n bounds: AnchorBounds;\n parent?: string;\n occupant?: string;\n attention: string[];\n }> {\n return Array.from(this.anchors.values()).map((a) => ({\n id: a.qualifiedId,\n gadgetId: a.gadgetId,\n bounds: a.bounds,\n parent: a.parent,\n occupant: a.occupant,\n attention: a.attention,\n }));\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gigabuddy/gadgets",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Gigabuddy Gadgets SDK — render interactive gadgets with breakout, state, and hot-swap",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/react.js CHANGED
@@ -40,6 +40,7 @@ function createGadgetRenderer(componentCode, data = {}, options) {
40
40
  const stateEnabled = options?.stateEnabled ?? false;
41
41
  const chatEnabled = options?.chatEnabled ?? false;
42
42
  const contextEnabled = options?.contextEnabled ?? false;
43
+ const roomEnabled = options?.roomEnabled ?? false;
43
44
  const escapedCode = componentCode.replace(/<\/script>/g, "<\\/script>");
44
45
  const serializedAssets = JSON.stringify(options?.assets ?? {});
45
46
  const stateBridgeScript = stateEnabled ? `
@@ -185,10 +186,183 @@ function createGadgetRenderer(componentCode, data = {}, options) {
185
186
  window.__rerenderGadget();
186
187
  }
187
188
  });` : "";
189
+ const roomBridgeScript = roomEnabled ? `
190
+ // \u2500\u2500 Room presence \u2500\u2500
191
+ window.gadget.room = {
192
+ peers: [],
193
+ userId: null,
194
+ displayName: null,
195
+ _peersCallbacks: [],
196
+ _playerJoinedCallbacks: [],
197
+ _playerLeftCallbacks: [],
198
+
199
+ setCursor: function(pos) {
200
+ window.parent.postMessage({ type: 'gadget-presence', cursor: pos }, '*');
201
+ },
202
+ setSelection: function(sel) {
203
+ window.parent.postMessage({ type: 'gadget-presence', selection: sel }, '*');
204
+ },
205
+ onPeersChange: function(cb) {
206
+ window.gadget.room._peersCallbacks.push(cb);
207
+ cb(window.gadget.room.peers);
208
+ return function() {
209
+ var i = window.gadget.room._peersCallbacks.indexOf(cb);
210
+ if (i !== -1) window.gadget.room._peersCallbacks.splice(i, 1);
211
+ };
212
+ },
213
+ onPlayerJoined: function(cb) {
214
+ window.gadget.room._playerJoinedCallbacks.push(cb);
215
+ return function() {
216
+ var i = window.gadget.room._playerJoinedCallbacks.indexOf(cb);
217
+ if (i !== -1) window.gadget.room._playerJoinedCallbacks.splice(i, 1);
218
+ };
219
+ },
220
+ onPlayerLeft: function(cb) {
221
+ window.gadget.room._playerLeftCallbacks.push(cb);
222
+ return function() {
223
+ var i = window.gadget.room._playerLeftCallbacks.indexOf(cb);
224
+ if (i !== -1) window.gadget.room._playerLeftCallbacks.splice(i, 1);
225
+ };
226
+ },
227
+ };
228
+
229
+ // \u2500\u2500 Room chat \u2500\u2500
230
+ window.gadget.roomChat = {
231
+ messages: [],
232
+ _messageCallbacks: [],
233
+
234
+ send: function(text) {
235
+ window.parent.postMessage({ type: 'gadget-chat-send', text: text }, '*');
236
+ },
237
+ onMessage: function(cb) {
238
+ window.gadget.roomChat._messageCallbacks.push(cb);
239
+ return function() {
240
+ var i = window.gadget.roomChat._messageCallbacks.indexOf(cb);
241
+ if (i !== -1) window.gadget.roomChat._messageCallbacks.splice(i, 1);
242
+ };
243
+ },
244
+ };
245
+
246
+ // \u2500\u2500 Gestures \u2500\u2500
247
+ window.gadget.gestures = {
248
+ active: [],
249
+ _spawnedCallbacks: [],
250
+ _dismissedCallbacks: [],
251
+
252
+ spawn: function(gadgetId, anchor, opts) {
253
+ opts = opts || {};
254
+ window.parent.postMessage({
255
+ type: 'gadget-gesture-send',
256
+ gadgetId: gadgetId,
257
+ anchor: anchor,
258
+ ttl: opts.ttl,
259
+ size: opts.size,
260
+ rotation: opts.rotation,
261
+ }, '*');
262
+ },
263
+ dismiss: function(gestureId) {
264
+ window.parent.postMessage({ type: 'gadget-gesture-dismiss', gestureId: gestureId }, '*');
265
+ },
266
+ reportAnchorRect: function(selector, rect) {
267
+ window.parent.postMessage({ type: 'gadget-anchor-rect', selector: selector, rect: rect }, '*');
268
+ },
269
+ onSpawned: function(cb) {
270
+ window.gadget.gestures._spawnedCallbacks.push(cb);
271
+ return function() {
272
+ var i = window.gadget.gestures._spawnedCallbacks.indexOf(cb);
273
+ if (i !== -1) window.gadget.gestures._spawnedCallbacks.splice(i, 1);
274
+ };
275
+ },
276
+ onDismissed: function(cb) {
277
+ window.gadget.gestures._dismissedCallbacks.push(cb);
278
+ return function() {
279
+ var i = window.gadget.gestures._dismissedCallbacks.indexOf(cb);
280
+ if (i !== -1) window.gadget.gestures._dismissedCallbacks.splice(i, 1);
281
+ };
282
+ },
283
+ };
284
+
285
+ // \u2500\u2500 Buddy attraction \u2500\u2500
286
+ window.gadget.buddy = {
287
+ attract: function(anchor, interest) {
288
+ window.parent.postMessage({
289
+ type: 'gadget-buddy-attract',
290
+ anchor: anchor,
291
+ interest: interest || 'medium',
292
+ }, '*');
293
+ },
294
+ };
295
+
296
+ window.addEventListener('message', function(event) {
297
+ var d = event.data;
298
+ if (!d) return;
299
+
300
+ // Presence updates
301
+ if (d.type === 'gadget-presence-update') {
302
+ window.gadget.room.peers = d.peers || [];
303
+ window.gadget.room._peersCallbacks.forEach(function(cb) { try { cb(d.peers || []); } catch(e) {} });
304
+ window.__rerenderGadget && window.__rerenderGadget();
305
+ }
306
+
307
+ // Player join/leave
308
+ if (d.type === 'gadget-player-joined') {
309
+ window.gadget.room._playerJoinedCallbacks.forEach(function(cb) { try { cb(d); } catch(e) {} });
310
+ }
311
+ if (d.type === 'gadget-player-left') {
312
+ window.gadget.room.peers = window.gadget.room.peers.filter(function(p) { return p.userId !== d.userId; });
313
+ window.gadget.room._playerLeftCallbacks.forEach(function(cb) { try { cb(d.userId); } catch(e) {} });
314
+ window.__rerenderGadget && window.__rerenderGadget();
315
+ }
316
+
317
+ // Chat
318
+ if (d.type === 'gadget-chat-message') {
319
+ var msg = d.message || d;
320
+ window.gadget.roomChat.messages.push(msg);
321
+ if (window.gadget.roomChat.messages.length > 100) window.gadget.roomChat.messages.shift();
322
+ window.gadget.roomChat._messageCallbacks.forEach(function(cb) { try { cb(msg); } catch(e) {} });
323
+ window.__rerenderGadget && window.__rerenderGadget();
324
+ }
325
+ if (d.type === 'gadget-chat-history') {
326
+ window.gadget.roomChat.messages = d.messages || [];
327
+ window.__rerenderGadget && window.__rerenderGadget();
328
+ }
329
+
330
+ // Gestures
331
+ if (d.type === 'gadget-gesture-spawned') {
332
+ window.gadget.gestures.active.push(d.gesture);
333
+ window.gadget.gestures._spawnedCallbacks.forEach(function(cb) { try { cb(d.gesture); } catch(e) {} });
334
+ window.__rerenderGadget && window.__rerenderGadget();
335
+ }
336
+ if (d.type === 'gadget-gesture-dismissed') {
337
+ window.gadget.gestures.active = window.gadget.gestures.active.filter(function(g) { return g.id !== d.gestureId; });
338
+ window.gadget.gestures._dismissedCallbacks.forEach(function(cb) { try { cb(d.gestureId); } catch(e) {} });
339
+ window.__rerenderGadget && window.__rerenderGadget();
340
+ }
341
+ if (d.type === 'gadget-gesture-sync') {
342
+ window.gadget.gestures.active = d.gestures || [];
343
+ window.__rerenderGadget && window.__rerenderGadget();
344
+ }
345
+
346
+ // Anchor rect request from host
347
+ if (d.type === 'gadget-request-anchor-rect' && d.selector) {
348
+ try {
349
+ var el = document.querySelector(d.selector);
350
+ if (el) {
351
+ var rect = el.getBoundingClientRect();
352
+ window.parent.postMessage({
353
+ type: 'gadget-anchor-rect',
354
+ selector: d.selector,
355
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
356
+ }, '*');
357
+ }
358
+ } catch(e) {}
359
+ }
360
+ });` : "";
188
361
  const chatPropFragment = chatEnabled ? ", chat: window.gadget.chat" : "";
189
362
  const contextPropFragment = contextEnabled ? ", context: window.gadget.context" : "";
363
+ const roomPropFragment = roomEnabled ? ", room: window.gadget.room, roomChat: window.gadget.roomChat, gestures: window.gadget.gestures" : "";
190
364
  const breakoutPropFragment = ", breakout: { active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }";
191
- const componentProps = stateEnabled ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }` : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment} }`;
365
+ const componentProps = stateEnabled ? `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__, state: window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined, userId: window.gadget.state ? window.gadget.state.userId : undefined${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }` : `{ data: window.__GADGET_DATA__, viewport: window.__GADGET_VIEWPORT__${chatPropFragment}${contextPropFragment}${breakoutPropFragment}${roomPropFragment} }`;
192
366
  return `<!DOCTYPE html>
193
367
  <html lang="en">
194
368
  <head>
@@ -271,6 +445,7 @@ function createGadgetRenderer(componentCode, data = {}, options) {
271
445
  ${stateBridgeScript}
272
446
  ${chatBridgeScript}
273
447
  ${contextBridgeScript}
448
+ ${roomBridgeScript}
274
449
 
275
450
  window.__gadgetRoot = null;
276
451
  window.__gadgetComponent = null;
@@ -352,7 +527,7 @@ ${contextBridgeScript}
352
527
  window.__gadgetRoot = root;
353
528
  root.render(
354
529
  <ErrorBoundary>
355
- <ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? " state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}" : ""}${chatEnabled ? " chat={window.gadget.chat}" : ""}${contextEnabled ? " context={window.gadget.context}" : ""} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />
530
+ <ComponentToRender data={window.__GADGET_DATA__} viewport={window.__GADGET_VIEWPORT__}${stateEnabled ? " state={window.gadget.state ? { shared: window.gadget.state.shared, user: window.gadget.state.user } : undefined} userId={window.gadget.state ? window.gadget.state.userId : undefined}" : ""}${chatEnabled ? " chat={window.gadget.chat}" : ""}${contextEnabled ? " context={window.gadget.context}" : ""}${roomEnabled ? " room={window.gadget.room} roomChat={window.gadget.roomChat} gestures={window.gadget.gestures}" : ""} breakout={{ active: window.gadget.breakout._active, originalRect: window.gadget.breakout._originalRect, request: window.gadget.breakout.request, exit: window.gadget.breakout.exit }} />
356
531
  </ErrorBoundary>
357
532
  );
358
533
  } else {
@@ -392,10 +567,43 @@ function GadgetFrame({
392
567
  messageId,
393
568
  className,
394
569
  onBreakoutChange,
395
- onError
570
+ onError,
571
+ lazy
396
572
  }) {
397
573
  const { config, resolveFileUrl } = useGadgetConfig();
398
574
  const base = `${config.apiUrl}/${config.orgId}/${config.projectId}`;
575
+ const lazyEnabled = lazy ?? mode === "compact";
576
+ const sentinelRef = useRef(null);
577
+ const [visible, setVisible] = useState(!lazyEnabled);
578
+ useEffect(() => {
579
+ if (!lazyEnabled || visible)
580
+ return;
581
+ const el = sentinelRef.current;
582
+ if (!el)
583
+ return;
584
+ const observer = new IntersectionObserver(
585
+ ([entry]) => {
586
+ if (entry.isIntersecting) {
587
+ setVisible(true);
588
+ observer.disconnect();
589
+ }
590
+ },
591
+ { rootMargin: "200px" }
592
+ // start loading 200px before entering viewport
593
+ );
594
+ observer.observe(el);
595
+ return () => observer.disconnect();
596
+ }, [lazyEnabled, visible]);
597
+ if (!visible) {
598
+ return /* @__PURE__ */ jsx2(
599
+ "div",
600
+ {
601
+ ref: sentinelRef,
602
+ className,
603
+ style: { width: "100%", height: maxHeight, minHeight: 60 }
604
+ }
605
+ );
606
+ }
399
607
  const [html, setHtml] = useState(null);
400
608
  const [loading, setLoading] = useState(true);
401
609
  const [error, setError] = useState(null);