diff --git a/apps/app/src/app/globals.css b/apps/app/src/app/globals.css index 7c6d573..b0e8d06 100644 --- a/apps/app/src/app/globals.css +++ b/apps/app/src/app/globals.css @@ -642,3 +642,8 @@ body, html { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } + +@keyframes fadeSlideUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/apps/app/src/components/generative-ui/charts/bar-chart.tsx b/apps/app/src/components/generative-ui/charts/bar-chart.tsx index aad11bc..26f80e6 100644 --- a/apps/app/src/components/generative-ui/charts/bar-chart.tsx +++ b/apps/app/src/components/generative-ui/charts/bar-chart.tsx @@ -62,7 +62,7 @@ export function BarChart({ title, description, data }: BarChartProps) { stroke="var(--chart-axis)" /> - + diff --git a/apps/app/src/components/generative-ui/charts/pie-chart.tsx b/apps/app/src/components/generative-ui/charts/pie-chart.tsx index 8701b84..8238928 100644 --- a/apps/app/src/components/generative-ui/charts/pie-chart.tsx +++ b/apps/app/src/components/generative-ui/charts/pie-chart.tsx @@ -70,7 +70,10 @@ export function PieChart({ title, description, data }: PieChartProps) { cx="50%" cy="50%" outerRadius={100} - isAnimationActive={false} + isAnimationActive={true} + animationDuration={800} + animationEasing="ease-out" + animationBegin={200} /> diff --git a/apps/app/src/components/generative-ui/idiomorph-inline.ts b/apps/app/src/components/generative-ui/idiomorph-inline.ts new file mode 100644 index 0000000..5375a0e --- /dev/null +++ b/apps/app/src/components/generative-ui/idiomorph-inline.ts @@ -0,0 +1,12 @@ +// idiomorph v0.3.0 — https://github.com/bigskysoftware/idiomorph +// Minified source inlined for use inside sandboxed iframes (no network access). +// +// To update: +// 1. npm install idiomorph@ (or download from GitHub releases) +// 2. Copy the minified IIFE build into the IDIOMORPH_JS string below +// 3. Update the version comment above +// 4. Verify: the global `Idiomorph.morph(target, newContent, opts)` must be available +// +// Do NOT edit the minified code by hand. + +export const IDIOMORPH_JS = `var Idiomorph=function(){"use strict";let o=new Set;let n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:t,afterNodeAdded:t,beforeNodeMorphed:t,afterNodeMorphed:t,beforeNodeRemoved:t,afterNodeRemoved:t,beforeAttributeUpdated:t},head:{style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:t,afterHeadMorphed:t}};function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=k(t)}let l=y(t);let r=p(e,l,n);return a(e,l,r)}function a(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=c(n,t,o);Promise.all(e).then(function(){a(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=M(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=d(r,e,o);if(e){return N(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function u(e,t){return t.ignoreActiveValue&&e===document.activeElement}function d(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return e;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!g(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return e;if(n.callbacks.beforeNodeAdded(t)===false)return e;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return e;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){c(t,e,n)}else{r(t,e,n);if(!u(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let a;while(i){a=i;i=a.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(a)===false)return;l.appendChild(a);r.callbacks.afterNodeAdded(a);H(r,a);continue}if(b(a,o,r)){d(o,a,r);o=o.nextSibling;H(r,a);continue}let e=A(n,l,a,o,r);if(e){o=v(o,e,r);d(e,a,r);H(r,a);continue}let t=S(n,l,a,o,r);if(t){o=v(o,t,r);d(t,a,r);H(r,a);continue}if(r.callbacks.beforeNodeAdded(a)===false)return;l.insertBefore(a,o);r.callbacks.afterNodeAdded(a);H(r,a)}while(o!==null){let e=o;o=o.nextSibling;T(e,r)}}function f(e,t,n,l){if(e==="value"&&l.ignoreActiveValue&&t===document.activeElement){return true}return l.callbacks.beforeAttributeUpdated(e,t,n)===false}function r(t,n,l){let e=t.nodeType;if(e===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(f(o.name,n,"update",l)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const a=i[e];if(f(a.name,n,"remove",l)){continue}if(!t.hasAttribute(a.name)){n.removeAttribute(a.name)}}}if(e===8||e===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!u(n,l)){s(t,n,l)}}function i(t,n,l,r){if(t[l]!==n[l]){let e=f(l,n,"update",r);if(!e){n[l]=t[l]}if(t[l]){if(!e){n.setAttribute(l,t[l])}}else{if(!f(l,n,"remove",r)){n.removeAttribute(l)}}}}function s(n,l,r){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked",r);i(n,l,"disabled",r);if(!n.hasAttribute("value")){if(!f("value",l,"remove",r)){l.value="";l.removeAttribute("value")}}else if(e!==t){if(!f("value",l,"update",r)){l.setAttribute("value",e);l.value=e}}}else if(n instanceof HTMLOptionElement){i(n,l,"selected",r)}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(f("value",l,"update",r)){return}if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function c(e,t,l){let r=[];let i=[];let o=[];let a=[];let u=l.head.style;let d=new Map;for(const n of e.children){d.set(n.outerHTML,n)}for(const s of t.children){let e=d.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{d.delete(s.outerHTML);o.push(s)}}else{if(u==="append"){if(t){i.push(s);a.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}a.push(...d.values());m("to append: ",a);let f=[];for(const c of a){m("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;m(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});f.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return f}function m(){}function t(){}function h(e){let t={};Object.assign(t,n);Object.assign(t,e);t.callbacks={};Object.assign(t.callbacks,n.callbacks);Object.assign(t.callbacks,e.callbacks);t.head={};Object.assign(t.head,n.head);Object.assign(t.head,e.head);return t}function p(e,t,n){n=h(n);return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:C(e,t),deadIds:new Set,callbacks:n.callbacks,head:n.head}}function b(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return L(n,e,t)>0}}return false}function g(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function v(t,e,n){while(t!==e){let e=t;t=t.nextSibling;T(e,n)}H(n,e);return e.nextSibling}function A(n,e,l,r,i){let o=L(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(b(l,e,i)){return e}t+=L(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function S(e,t,n,l,r){let i=l;let o=n.nextSibling;let a=0;while(i!=null){if(L(r,i,e)>0){return null}if(g(n,i)){return i}if(g(o,i)){a++;o=o.nextSibling;if(a>=2){return null}}i=i.nextSibling}return i}function k(n){let l=new DOMParser;let e=n.replace(/]*>|>)([\\s\\S]*?)<\\/svg>/gim,"");if(e.match(/<\\/html>/)||e.match(/<\\/head>/)||e.match(/<\\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function y(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function N(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function M(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=w(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function w(e,t,n){if(g(e,t)){return.5+L(n,e,t)}return 0}function T(e,t){H(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function E(e,t){return!e.deadIds.has(t)}function x(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function H(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function L(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(E(e,i)&&x(e,i,n)){++r}}return r}function R(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function C(e,t){let n=new Map;R(e,n);R(t,n);return n}return{morph:e,defaults:n}}();`; diff --git a/apps/app/src/components/generative-ui/meeting-time-picker.tsx b/apps/app/src/components/generative-ui/meeting-time-picker.tsx index e765c38..0d01c82 100644 --- a/apps/app/src/components/generative-ui/meeting-time-picker.tsx +++ b/apps/app/src/components/generative-ui/meeting-time-picker.tsx @@ -98,6 +98,10 @@ export function MeetingTimePicker({ hover:scale-102 active:scale-98 flex justify-between items-center hover:bg-blue-50 dark:hover:bg-blue-900/30" + style={{ + animation: "fadeSlideUp 0.4s ease-out both", + animationDelay: `${index * 80}ms`, + }} >
{slot.date}
@@ -115,6 +119,10 @@ export function MeetingTimePicker({ text-gray-600 dark:text-zinc-400 hover:text-gray-800 dark:hover:text-zinc-200 transition-all cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700" + style={{ + animation: "fadeSlideUp 0.4s ease-out both", + animationDelay: `${slots.length * 80}ms`, + }} > None of these work diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx index 15c7fa7..1e45b51 100644 --- a/apps/app/src/components/generative-ui/widget-renderer.tsx +++ b/apps/app/src/components/generative-ui/widget-renderer.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState, useCallback } from "react"; import { z } from "zod"; import { SaveTemplateOverlay } from "./save-template-overlay"; +import { IDIOMORPH_JS } from "./idiomorph-inline"; // ─── Zod Schema (CopilotKit parameter contract) ───────────────────── export const WidgetRendererProps = z.object({ @@ -302,16 +303,21 @@ input[type="checkbox"], input[type="radio"] { a { color: var(--color-text-info); text-decoration: none; } a:hover { text-decoration: underline; } -/* Progressive reveal for content children */ -#content > * { +/* First render: stagger all children */ +#content.initial-render > * { + animation: fadeSlideIn 0.4s ease-out both; +} +#content.initial-render > *:nth-child(1) { animation-delay: 0s; } +#content.initial-render > *:nth-child(2) { animation-delay: 0.06s; } +#content.initial-render > *:nth-child(3) { animation-delay: 0.12s; } +#content.initial-render > *:nth-child(4) { animation-delay: 0.18s; } +#content.initial-render > *:nth-child(5) { animation-delay: 0.24s; } +#content.initial-render > *:nth-child(n+6) { animation-delay: 0.3s; } + +/* Subsequent morphs: only new elements animate in */ +.morph-enter { animation: fadeSlideIn 0.4s ease-out both; } -#content > *:nth-child(1) { animation-delay: 0s; } -#content > *:nth-child(2) { animation-delay: 0.06s; } -#content > *:nth-child(3) { animation-delay: 0.12s; } -#content > *:nth-child(4) { animation-delay: 0.18s; } -#content > *:nth-child(5) { animation-delay: 0.24s; } -#content > *:nth-child(n+6) { animation-delay: 0.3s; } @keyframes fadeSlideIn { from { opacity: 0; transform: translateY(8px); } @@ -352,32 +358,72 @@ window.addEventListener('message', function(e) { if (e.source !== window.parent) return; if (e.data && e.data.type === 'update-content') { var content = document.getElementById('content'); - if (content) { - // Strip script tags from HTML before inserting — scripts are handled separately below - var tmp = document.createElement('div'); - tmp.innerHTML = e.data.html; - var incomingScripts = []; - tmp.querySelectorAll('script').forEach(function(s) { - incomingScripts.push({ src: s.src, text: s.textContent }); - s.remove(); - }); - content.innerHTML = tmp.innerHTML; - - // Execute only new scripts (not previously executed) - incomingScripts.forEach(function(scriptInfo) { - var key = scriptInfo.src || scriptInfo.text; - if (content.getAttribute('data-exec-' + btoa(key).slice(0, 16))) return; - content.setAttribute('data-exec-' + btoa(key).slice(0, 16), '1'); - var newScript = document.createElement('script'); - if (scriptInfo.src) { - newScript.src = scriptInfo.src; - } else { - newScript.textContent = scriptInfo.text; - } - content.appendChild(newScript); - }); + if (!content) return; + + // Strip script tags from HTML before inserting — scripts are handled separately below + var tmp = document.createElement('div'); + tmp.innerHTML = e.data.html; + var incomingScripts = []; + tmp.querySelectorAll('script').forEach(function(s) { + incomingScripts.push({ src: s.src, text: s.textContent }); + s.remove(); + }); + + // Reset tracking when content is cleared (new streaming session) + if (!tmp.innerHTML.trim()) { + content.removeAttribute('data-has-content'); + content.innerHTML = ''; reportHeight(); + return; } + + // First render: add stagger class for initial entrance animation + var isFirstRender = !content.hasAttribute('data-has-content'); + if (isFirstRender) { + content.classList.add('initial-render'); + content.setAttribute('data-has-content', '1'); + setTimeout(function() { content.classList.remove('initial-render'); }, 800); + } + + // Use idiomorph to diff/patch DOM (preserves existing nodes, no flicker) + if (window.Idiomorph) { + try { + Idiomorph.morph(content, tmp.innerHTML, { + morphStyle: 'innerHTML', + callbacks: { + beforeNodeAdded: function(node) { + // Tag new element nodes for entrance animation + if (node.nodeType === 1) { + node.classList.add('morph-enter'); + node.addEventListener('animationend', function() { + node.classList.remove('morph-enter'); + }, { once: true }); + } + } + } + }); + } catch (err) { + // Fallback: full replacement on morph failure + content.innerHTML = tmp.innerHTML; + } + } else { + content.innerHTML = tmp.innerHTML; + } + + // Execute only new scripts (not previously executed) + incomingScripts.forEach(function(scriptInfo) { + var key = scriptInfo.src || scriptInfo.text; + if (content.getAttribute('data-exec-' + btoa(key).slice(0, 16))) return; + content.setAttribute('data-exec-' + btoa(key).slice(0, 16), '1'); + var newScript = document.createElement('script'); + if (scriptInfo.src) { + newScript.src = scriptInfo.src; + } else { + newScript.textContent = scriptInfo.text; + } + content.appendChild(newScript); + }); + reportHeight(); } }); @@ -425,6 +471,7 @@ function assembleShell(initialHtml: string = ""): string {
${initialHtml}
+