@@ -301,6 +301,22 @@ input[type="checkbox"], input[type="radio"] {
301301a { color: var(--color-text-info); text-decoration: none; }
302302a:hover { text-decoration: underline; }
303303
304+ /* Progressive reveal for content children */
305+ #content > * {
306+ animation: fadeSlideIn 0.4s ease-out both;
307+ }
308+ #content > *:nth-child(1) { animation-delay: 0s; }
309+ #content > *:nth-child(2) { animation-delay: 0.06s; }
310+ #content > *:nth-child(3) { animation-delay: 0.12s; }
311+ #content > *:nth-child(4) { animation-delay: 0.18s; }
312+ #content > *:nth-child(5) { animation-delay: 0.24s; }
313+ #content > *:nth-child(n+6) { animation-delay: 0.3s; }
314+
315+ @keyframes fadeSlideIn {
316+ from { opacity: 0; transform: translateY(8px); }
317+ to { opacity: 1; transform: translateY(0); }
318+ }
319+
304320@media (prefers-reduced-motion: reduce) {
305321 *, *::before, *::after {
306322 animation-duration: 0.01ms !important;
@@ -332,11 +348,12 @@ document.addEventListener('click', function(e) {
332348
333349// Auto-resize: report content height to host
334350function reportHeight() {
335- var h = document.documentElement.scrollHeight;
351+ var content = document.getElementById('content');
352+ var h = content ? content.offsetHeight : document.documentElement.scrollHeight;
336353 window.parent.postMessage({ type: 'widget-resize', height: h }, '*');
337354}
338355var ro = new ResizeObserver(reportHeight);
339- ro.observe(document.body);
356+ ro.observe(document.getElementById('content') || document. body);
340357window.addEventListener('load', reportHeight);
341358// Periodic reports during initial load
342359var _resizeInterval = setInterval(reportHeight, 200);
@@ -382,11 +399,18 @@ function assembleDocument(html: string): string {
382399// ─── React Component ─────────────────────────────────────────────────
383400export function WidgetRenderer ( { title, description, html } : WidgetRendererProps ) {
384401 const iframeRef = useRef < HTMLIFrameElement > ( null ) ;
385- const [ height , setHeight ] = useState ( 500 ) ;
402+ const [ height , setHeight ] = useState ( 0 ) ;
403+ const [ loaded , setLoaded ] = useState ( false ) ;
386404
387405 const handleMessage = useCallback ( ( e : MessageEvent ) => {
388- if ( e . data ?. type === "widget-resize" && typeof e . data . height === "number" ) {
389- setHeight ( Math . max ( 100 , Math . min ( e . data . height + 8 , 4000 ) ) ) ;
406+ // Only handle messages from our own iframe
407+ if (
408+ iframeRef . current &&
409+ e . source === iframeRef . current . contentWindow &&
410+ e . data ?. type === "widget-resize" &&
411+ typeof e . data . height === "number"
412+ ) {
413+ setHeight ( Math . max ( 50 , Math . min ( e . data . height + 8 , 4000 ) ) ) ;
390414 }
391415 } , [ ] ) ;
392416
@@ -395,22 +419,47 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
395419 return ( ) => window . removeEventListener ( "message" , handleMessage ) ;
396420 } , [ handleMessage ] ) ;
397421
398- if ( ! html ) {
399- return null ;
400- }
422+ // Reset loaded state when html changes
423+ useEffect ( ( ) => {
424+ setLoaded ( false ) ;
425+ setHeight ( 0 ) ;
426+ } , [ html ] ) ;
427+
428+ // Iframe is ready when it has loaded AND reported a valid height
429+ const ready = loaded && height > 0 ;
401430
402- const srcdoc = assembleDocument ( html ) ;
431+ const srcdoc = html ? assembleDocument ( html ) : "" ;
403432
404433 return (
405434 < div className = "w-full my-3" >
406- < iframe
407- ref = { iframeRef }
408- srcDoc = { srcdoc }
409- sandbox = "allow-scripts allow-same-origin"
410- className = "w-full border-0"
411- style = { { height, overflow : "hidden" , background : "transparent" } }
412- title = { title }
413- />
435+ { /* Skeleton: visible until iframe is fully ready */ }
436+ { ! ready && (
437+ < div className = "animate-pulse" >
438+ < div className = "space-y-3 py-4" >
439+ < div className = "h-4 bg-gray-200 dark:bg-zinc-700 rounded w-3/4" />
440+ < div className = "h-32 bg-gray-200 dark:bg-zinc-700 rounded" />
441+ < div className = "h-4 bg-gray-200 dark:bg-zinc-700 rounded w-1/2" />
442+ </ div >
443+ </ div >
444+ ) }
445+ { /* Iframe: rendered when html exists, but invisible until ready */ }
446+ { html && (
447+ < iframe
448+ ref = { iframeRef }
449+ srcDoc = { srcdoc }
450+ sandbox = "allow-scripts allow-same-origin"
451+ className = "w-full border-0"
452+ onLoad = { ( ) => setLoaded ( true ) }
453+ style = { {
454+ height : ready ? height : 0 ,
455+ overflow : "hidden" ,
456+ background : "transparent" ,
457+ opacity : ready ? 1 : 0 ,
458+ transition : "opacity 300ms ease-in" ,
459+ } }
460+ title = { title }
461+ />
462+ ) }
414463 </ div >
415464 ) ;
416465}
0 commit comments