diff --git a/README.md b/README.md index 6d07488..0636726 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,22 @@ createUI('dashboard', require.resolve('./Dashboard.tsx')); // CommonJS - **`useProps(defaults)`** — Get props passed from the server - **`sendPrompt(message)`** — Send a message to the AI chat - **`callTool(name, params)`** — Invoke an MCP tool +- **`resizeToContent(element?)`** — Request parent to resize iframe to fit content +- **`useResizeToContent()`** — Hook that auto-notifies parent when content size changes + +```tsx +import { useResizeToContent } from 'mcp-ui-kit/ui'; + +function MyComponent() { + const containerRef = useResizeToContent(); + + return ( +
+ {/* Content that may change size */} +
+ ); +} +``` ## Development diff --git a/nanobot.db b/nanobot.db index 1ae7308..58a605a 100644 Binary files a/nanobot.db and b/nanobot.db differ diff --git a/package.json b/package.json index 1cd2eb3..6a8c5b9 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "scripts": { "dev": "npm run start --workspace=demo-server & npm run dev --workspace=@mcp-ui-kit/inspector", "start": "npm run start --workspace=demo-server", + "build": "npm run build --workspace=packages/library", "start:inspector": "npm run dev --workspace=@mcp-ui-kit/inspector", - "publish:library": "npm publish --workspace=packages/library" + "publish:library": "npm publish --workspace=packages/library", + "nanobot": "export $(cat .env | xargs) && nanobot run ./nanobot.yaml" }, "devDependencies": { "typescript": "^5.6.3" diff --git a/packages/demo-server/components/StockDashboard.tsx b/packages/demo-server/components/StockDashboard.tsx index 242c30c..638855d 100644 --- a/packages/demo-server/components/StockDashboard.tsx +++ b/packages/demo-server/components/StockDashboard.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { generateMockStockData } from './stock-utils'; -import { callTool, sendPrompt, useProps } from 'mcp-ui-kit/ui'; +import { callTool, sendPrompt, useProps, useResizeToContent } from 'mcp-ui-kit/ui'; // Types for props passed from the tool handler @@ -31,6 +31,9 @@ export function StockDashboard() { timeframe: '1M', }); + // Auto-notify parent when content size changes + const containerRef = useResizeToContent(); + const stocks: StockData[] = generateMockStockData(props.symbols); const [selectedStock, setSelectedStock] = useState(stocks[0]?.symbol || ''); @@ -70,7 +73,7 @@ Please analyze this portfolio and provide recommendations.`; return ( <> -
+
{/* Header */}
diff --git a/packages/demo-server/components/WeatherDashboard.tsx b/packages/demo-server/components/WeatherDashboard.tsx index 2be2803..2007225 100644 --- a/packages/demo-server/components/WeatherDashboard.tsx +++ b/packages/demo-server/components/WeatherDashboard.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { styles } from './styles'; +import { useResizeToContent } from 'mcp-ui-kit/ui'; interface WeatherData { temp: number; @@ -35,6 +36,7 @@ const weatherDataByCity: WeatherData = { export function WeatherDashboard() { + const containerRef = useResizeToContent(); const data = weatherDataByCity; const currentDate = new Date().toLocaleDateString('en-US', { weekday: 'long', @@ -60,7 +62,7 @@ ${data.forecast.map(day => `${day.day}: ${day.icon} ${day.temp}°C`).join('\n')} return ( <> -
+

diff --git a/packages/library/ui/index.ts b/packages/library/ui/index.ts index 39f61bf..eae3452 100644 --- a/packages/library/ui/index.ts +++ b/packages/library/ui/index.ts @@ -1,3 +1,4 @@ +import { useEffect, useRef, type RefObject } from 'react'; /** * Send a prompt to the parent window @@ -24,6 +25,64 @@ export function callTool(toolName: string, params: Record = {}) }, '*'); } +/** + * Request the parent to resize the iframe to fit content + * Call this after your content has rendered + */ +export function resizeToContent(element?: HTMLElement) { + const target = element || document.body; + + // Use scrollHeight/scrollWidth for full content size (not just visible area) + // Also account for any margin on the element + const style = window.getComputedStyle(target); + const marginTop = parseFloat(style.marginTop) || 0; + const marginBottom = parseFloat(style.marginBottom) || 0; + const marginLeft = parseFloat(style.marginLeft) || 0; + const marginRight = parseFloat(style.marginRight) || 0; + + // For body, also account for body padding/margin + let extraHeight = 0; + let extraWidth = 0; + if (target !== document.body) { + const bodyStyle = window.getComputedStyle(document.body); + extraHeight = (parseFloat(bodyStyle.paddingTop) || 0) + (parseFloat(bodyStyle.paddingBottom) || 0); + extraWidth = (parseFloat(bodyStyle.paddingLeft) || 0) + (parseFloat(bodyStyle.paddingRight) || 0); + } + + const width = Math.ceil(target.scrollWidth + marginLeft + marginRight + extraWidth); + const height = Math.ceil(target.scrollHeight + marginTop + marginBottom + extraHeight); + + window.parent.postMessage({ + type: 'resize', + payload: { width, height } + }, '*'); +} + +/** + * Hook that automatically notifies parent when content size changes + * Returns a ref to attach to your container element + */ +export function useResizeToContent(): RefObject { + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + const observer = new ResizeObserver(() => { + resizeToContent(element); + }); + + observer.observe(element); + // Initial resize + resizeToContent(element); + + return () => observer.disconnect(); + }, []); + + return ref; +} + /** * Use MCP props */