{"schemaVersion":1,"id":"recommended.sm-pipeline","version":"1.0.0","name":"Canvas workflow","description":"An editable production workflow using Canvas v2.2.0.","stages":[{"id":"main","blockId":"sm.pipeline","version":"2.2.0","label":"Canvas"}],"connections":[],"packages":[{"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"sm.pipeline","version":"2.2.0","name":"Canvas","description":"Edit the complete StillMade Canvas workspace and attach exact generated image, video, and narration versions while preserving Project Type Steps and connections.","kind":"workspace","runtime":"javascript","entry":"src/run.js","license":"MIT","provenance":{"notice":"Original source (authorship declared by the packager)\nLicense: MIT\nChanges: Original implementation declared by the packager; no reused repository or dependency code.\nFiles: LICENSE, src/run.js, src/view.json\n\nMIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","portableSources":[{"kind":"original","license":"MIT","paths":["LICENSE","src/run.js","src/view.json"],"evidence":["upstream/0/LICENSE","upstream/0/src/run.js","upstream/0/src/view.json"],"changes":"Original implementation declared by the packager; no reused repository or dependency code."}]},"inputs":{"canvas":{"type":"pipeline","context":"canvas","primary":true,"required":false,"default":{"schemaVersion":1,"nodes":[],"edges":[]},"description":"The complete safe Canvas projection. Native content and layout are retained; executable source and workflow history remain host-owned."},"updatedCanvas":{"type":"pipeline","required":false,"default":{"schemaVersion":1,"nodes":[],"edges":[]},"description":"The complete edited Canvas projection prepared in this workspace."},"commit":{"type":"boolean","default":false,"description":"Prepare the edited Canvas for StillMade review and Apply."},"generatedImage":{"type":"image","imageMode":"reference","required":false,"description":"An exact stored image version produced by an approved generation Block or selected from project media."},"addGeneratedImage":{"type":"boolean","default":false,"description":"Add the connected image as a native Canvas media node."},"generatedImageTitle":{"type":"text","required":false,"default":"Generated image","description":"Label for the connected image on Canvas."},"generatedVideo":{"type":"video","required":false,"description":"An exact stored video version produced by an approved generation Block or selected from project media."},"addGeneratedVideo":{"type":"boolean","default":false,"description":"Add the connected video as a native Canvas media node."},"generatedVideoTitle":{"type":"text","required":false,"default":"Generated video","description":"Label for the connected video on Canvas."},"generatedAudio":{"type":"audio","required":false,"description":"An exact stored narration version produced by an approved Voiceover Block or selected from project media."},"addGeneratedAudio":{"type":"boolean","default":false,"description":"Add the connected narration as a native Canvas voiceover node."},"generatedAudioTitle":{"type":"text","required":false,"default":"Voiceover","description":"Label for the connected narration on Canvas."}},"outputs":{"canvas":{"type":"pipeline","primary":true,"role":"canvas_document","description":"The complete edited safe Canvas projection."},"edit":{"type":"workspace-edit","description":"An exact before/after Canvas change reviewed before it updates the project."}},"permissions":{"project":["context.canvas.read","workspace.canvas.propose"],"network":[],"filesystem":[],"secrets":[]},"ui":[],"platforms":{"phone":{"supported":true},"browser":{"supported":true},"desktop":{"supported":true}}},"code":"const copy=value=>JSON.parse(JSON.stringify(value));\nconst source=copy(input.canvas||{schemaVersion:1,nodes:[],edges:[]});\nconst candidate=input.commit?copy(input.updatedCanvas):copy(source);\nif(!candidate||Array.isArray(candidate)||candidate.schemaVersion!==1||!Array.isArray(candidate.nodes)||!Array.isArray(candidate.edges))throw new Error('Keep the complete version 1 Canvas document with nodes and edges.');\nconst attach=(value,kind,title)=>{\n if(!value||value.kind!==kind)throw new Error('Connect an approved generated '+kind+' before adding it to Canvas.');\n const ref=copy(value),base='connected-'+kind+'-'+ref.assetId+'-'+ref.versionId;\n let id=base,suffix=2;while(candidate.nodes.some(node=>node.id===id)){id=base+'-'+suffix;suffix+=1}\n const position={x:80+candidate.nodes.length*24,y:80+candidate.nodes.length*24};\n if(kind==='image')candidate.nodes.push({id,type:'imageGenNode',position,width:320,height:320,data:{title:title||'Generated image',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId}],activeVersion:0,selectedVersion:0,status:'complete',source:'connected-block'}});\n if(kind==='video')candidate.nodes.push({id,type:'videoGenNode',position,width:360,height:260,data:{title:title||'Generated video',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,approvedVersion:0,status:'complete',source:'connected-block'}});\n if(kind==='audio')candidate.nodes.push({id,type:'voiceoverGenNode',position,width:360,height:260,data:{title:title||'Voiceover',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,status:'complete',source:'connected-block'}});\n};\nif(input.addGeneratedImage)attach(input.generatedImage,'image',input.generatedImageTitle);\nif(input.addGeneratedVideo)attach(input.generatedVideo,'video',input.generatedVideoTitle);\nif(input.addGeneratedAudio)attach(input.generatedAudio,'audio',input.generatedAudioTitle);\nif(candidate.nodes.length>5000||candidate.edges.length>10000)throw new Error('Canvas supports up to 5,000 nodes and 10,000 connections.');\nif(!candidate.nodes.every(node=>node&&typeof node==='object'&&!Array.isArray(node)&&typeof node.id==='string'&&node.id&&typeof node.type==='string'&&node.type&&node.position&&Number.isFinite(node.position.x)&&Number.isFinite(node.position.y)))throw new Error('Every Canvas node needs an identity, type, and finite position.');\nif(new Set(candidate.nodes.map(node=>node.id)).size!==candidate.nodes.length)throw new Error('Canvas node identities must be distinct.');\nconst ids=new Set(candidate.nodes.map(node=>node.id));\nif(!candidate.edges.every(edge=>edge&&typeof edge==='object'&&!Array.isArray(edge)&&typeof edge.id==='string'&&edge.id&&ids.has(edge.source)&&ids.has(edge.target)))throw new Error('Every Canvas connection needs an identity and existing source and target nodes.');\nif(new Set(candidate.edges.map(edge=>edge.id)).size!==candidate.edges.length)throw new Error('Canvas connection identities must be distinct.');\nreturn{canvas:candidate,edit:{schemaVersion:1,title:`Update Canvas with ${candidate.nodes.length} node${candidate.nodes.length===1?'':'s'}`,field:'canvas',before:source,after:candidate}};\n","view":{"html":"<main class=\"canvas-workspace\">\n  <header><div><small>Production workspace</small><h1>Canvas</h1><p id=\"summary\">Loading the complete Canvas…</p></div><div class=\"actions\"><button id=\"reset\" type=\"button\" class=\"secondary\">Reset changes</button><button id=\"add\" type=\"button\" class=\"secondary\">Add note</button><button id=\"save\" type=\"button\">Review changes</button></div></header>\n  <output id=\"status\" role=\"status\" aria-live=\"polite\"></output>\n  <section><div class=\"section-title\"><h2>Connected media</h2></div><div class=\"handoffs\"><div id=\"handoff-image\"></div><div id=\"handoff-video\"></div><div id=\"handoff-audio\"></div></div></section>\n  <section><h2>Canvas items</h2><p class=\"hint\">Project Type Steps stay connected and carry their exact installed source. Edit their placement or label here; use the Project Type builder to add, remove, replace, or reconnect Steps.</p><div id=\"nodes\" class=\"cards\"></div></section>\n  <section><h2>Connections</h2><div id=\"edges\" class=\"connections\"></div></section>\n  <details><summary>Complete Canvas data</summary><p>Native content, media references, dimensions, and custom fields are retained. Executable source and host bindings are protected when StillMade applies the proposal.</p><textarea id=\"raw\" class=\"raw\" spellcheck=\"false\"></textarea><button id=\"applyRaw\" type=\"button\" class=\"secondary\">Apply JSON to workspace</button></details>\n</main>\n","css":".canvas-workspace{max-width:1120px;margin:0 auto;padding:8px;color:var(--fg)}header,.actions,.card-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}header{justify-content:space-between;margin-bottom:14px}header h1{margin:2px 0}header p,header small,details p,output,.hint{color:var(--fg-muted)}section,details{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3);background:var(--bg-elev-1)}section h2{margin:0 0 8px}.cards,.connections{display:grid;gap:10px}.handoffs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.handoff,.card,.connection{padding:12px;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg)}.handoff p{min-height:38px}.card-head{justify-content:space-between}.card h3{margin:0;font-size:16px}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}label{display:grid;gap:5px;margin:7px 0;font-size:13px;font-weight:600}input,textarea{width:100%;background:var(--bg)}textarea{min-height:76px;resize:vertical}.wide{grid-column:1/-1}.secondary{background:transparent;color:var(--fg)}.danger{background:transparent;color:var(--signal)}.raw{min-height:420px;font-family:var(--font-mono);font-size:12px}output{display:block;min-height:24px;margin:8px 2px}.badge{font-size:12px;color:var(--fg-muted)}\n@media(max-width:760px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.canvas-workspace{padding:0}.grid,.handoffs{grid-template-columns:1fr}.actions{width:100%}.actions button{flex:1}section,details{padding:12px}}\n","javascript":"const status=document.getElementById('status'),summary=document.getElementById('summary'),raw=document.getElementById('raw'),save=document.getElementById('save');\nlet source={schemaVersion:1,nodes:[],edges:[]},draft={schemaVersion:1,nodes:[],edges:[]},connectedImage=null,connectedVideo=null,connectedAudio=null,titles={image:'Generated image',video:'Generated video',audio:'Voiceover'},busy=false,revision=0;\nconst clone=value=>JSON.parse(JSON.stringify(value));\nconst text=value=>value===undefined||value===null?'':String(value);\nconst managed=node=>node.type==='programmableBlockNode'||!!(node.data&&node.data.hostBlock);\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},{tag:'input',type,action,value:Number.isFinite(value)?value:text(value),disabled:busy,label}]});\nconst nodeCard=(node,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(node.data&&node.data.title)||text(node.data&&node.data.label)||text(node.data&&node.data.name)||text(node.id)},{tag:'span',className:'badge',text:managed(node)?'Project Type Step':text(node.type)},{tag:'button',action:'remove:'+String(index),text:'Remove',className:'danger',disabled:busy||managed(node)}]},{tag:'div',className:'grid',children:[field('Title','title:'+String(index),node.data&&node.data.title),field('Label','label:'+String(index),node.data&&node.data.label),field('X','x:'+String(index),node.position&&node.position.x,'number'),field('Y','y:'+String(index),node.position&&node.position.y,'number'),field('Width','width:'+String(index),node.width,'number'),field('Height','height:'+String(index),node.height,'number'),managed(node)?{tag:'p',className:'wide hint',text:'Its executable source and connections are preserved.'}:field('Content','content:'+String(index),node.data&&(node.data.text===undefined?node.data.content:node.data.text),'text',true)]}]});\nconst edgeCard=edge=>({tag:'article',className:'connection',children:[{tag:'strong',text:text(edge.source)+' → '+text(edge.target)},{tag:'p',className:'hint',text:text(edge.sourceHandle)+' '+text(edge.targetHandle)}]});\nconst ensure=()=>{if(!draft||Array.isArray(draft)||draft.schemaVersion!==1)draft=clone(source);if(!Array.isArray(draft.nodes))draft.nodes=[];if(!Array.isArray(draft.edges))draft.edges=[]};\nconst handoff=(kind,value,label)=>value?[{tag:'article',className:'handoff',children:[{tag:'strong',text:label},{tag:'p',className:'hint',text:text(value.assetId)+' · version '+text(value.versionId)},{tag:'button',action:'attach-'+kind,text:'Add '+kind+' to Canvas',className:'secondary',disabled:busy}]}]:[{tag:'article',className:'handoff',children:[{tag:'strong',text:label},{tag:'p',className:'hint',text:'Connect a stored '+kind+' output to add it with exact asset and version identity.'}]}];\nconst draw=()=>{ensure();StillMade.render('handoff-image',handoff('image',connectedImage,'Image'));StillMade.render('handoff-video',handoff('video',connectedVideo,'Video'));StillMade.render('handoff-audio',handoff('audio',connectedAudio,'Narration'));StillMade.render('nodes',draft.nodes.length?draft.nodes.map(nodeCard):[{tag:'p',text:'No Canvas items yet.'}]);StillMade.render('edges',draft.edges.length?draft.edges.map(edgeCard):[{tag:'p',text:'No connections.'}]);raw.value=JSON.stringify(draft,null,2);summary.textContent=String(draft.nodes.length)+' item'+(draft.nodes.length===1?'':'s')+' · '+String(draft.edges.length)+' connection'+(draft.edges.length===1?'':'s');save.disabled=busy;document.getElementById('reset').disabled=busy;document.getElementById('add').disabled=busy};\nconst changed=()=>{revision+=1;raw.value=JSON.stringify(draft,null,2);status.textContent='Changes are local until you choose Review changes.'};\nconst attach=(kind,ref,title)=>{const id='connected-'+kind+'-'+ref.assetId+'-'+ref.versionId+'-'+String(revision+1),position={x:80+draft.nodes.length*24,y:80+draft.nodes.length*24};let node;if(kind==='image')node={id,type:'imageGenNode',position,width:320,height:320,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId}],activeVersion:0,selectedVersion:0,status:'complete',source:'connected-block'}};if(kind==='video')node={id,type:'videoGenNode',position,width:360,height:260,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,approvedVersion:0,status:'complete',source:'connected-block'}};if(kind==='audio')node={id,type:'voiceoverGenNode',position,width:360,height:260,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,status:'complete',source:'connected-block'}};draft.nodes=[...draft.nodes,node];changed();draw();status.textContent='Connected '+kind+' added with its exact asset and version identity.'};\nStillMade.onAction(event=>{if(busy)return;ensure();const parts=event.action.split(':'),kind=parts[0],index=Number(parts[1]);if(kind==='attach-image'&&connectedImage){attach('image',connectedImage,titles.image);return}if(kind==='attach-video'&&connectedVideo){attach('video',connectedVideo,titles.video);return}if(kind==='attach-audio'&&connectedAudio){attach('audio',connectedAudio,titles.audio);return}const node=draft.nodes.find((item,position)=>position===index);if(!node)return;if(kind==='remove'&&!managed(node)){const id=node.id;draft.nodes=draft.nodes.filter((item,position)=>position!==index);draft.edges=draft.edges.filter(edge=>edge.source!==id&&edge.target!==id);changed();draw();return}if(!node.data||Array.isArray(node.data))node.data={};if(kind==='title')node.data.title=event.value;if(kind==='label')node.data.label=event.value;if(kind==='x')node.position.x=Number(event.value)||0;if(kind==='y')node.position.y=Number(event.value)||0;if(kind==='width')node.width=Math.max(1,Number(event.value)||1);if(kind==='height')node.height=Math.max(1,Number(event.value)||1);if(kind==='content'&&!managed(node)){if(node.data.text!==undefined)node.data.text=event.value;else node.data.content=event.value}changed()});\ndocument.getElementById('add').addEventListener('click',()=>{if(busy)return;ensure();const id='portable-note-'+String(draft.nodes.length+1)+'-'+String(revision+1);draft.nodes=[...draft.nodes,{id,type:'stickyNote',position:{x:80+draft.nodes.length*24,y:80+draft.nodes.length*24},width:260,height:180,data:{title:'Note',text:'New note'}}];changed();draw()});\ndocument.getElementById('applyRaw').addEventListener('click',()=>{if(busy)return;try{const value=JSON.parse(raw.value);if(!value||Array.isArray(value)||value.schemaVersion!==1||!Array.isArray(value.nodes)||!Array.isArray(value.edges))throw Error('Canvas JSON must be a version 1 document with nodes and edges.');draft=clone(value);changed();draw();status.textContent='Complete Canvas data applied locally.'}catch(error){status.textContent=error.message}});\ndocument.getElementById('reset').addEventListener('click',()=>{if(!busy){draft=clone(source);revision+=1;draw();status.textContent='Changes reset to the current Canvas.'}});\nsave.addEventListener('click',async()=>{if(busy)return;const started=revision;busy=true;draw();status.textContent='Checking the complete Canvas…';try{const result=await StillMade.run({canvas:source,updatedCanvas:draft,commit:true});draft=clone(result.canvas);if(started===revision)status.textContent='Canvas changes are ready below. Review the before and after, then choose Apply.'}catch(error){status.textContent=error.message||'Canvas changes could not be prepared.'}finally{busy=false;draw()}});\nStillMade.onInput(values=>{source=clone(values.canvas||source);draft=clone(source);connectedImage=values.generatedImage?clone(values.generatedImage):null;connectedVideo=values.generatedVideo?clone(values.generatedVideo):null;connectedAudio=values.generatedAudio?clone(values.generatedAudio):null;titles={image:values.generatedImageTitle||'Generated image',video:values.generatedVideoTitle||'Generated video',audio:values.generatedAudioTitle||'Voiceover'};revision+=1;draw();status.textContent='Edit the complete Canvas. Project Type Steps remain protected as whole Blocks.'});\n"},"tests":[{"name":"Retain native media and protected Steps","input":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]},"updatedCanvas":{"schemaVersion":1,"nodes":[],"edges":[]},"commit":false},"expected":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]},"edit":{"schemaVersion":1,"title":"Update Canvas with 2 nodes","field":"canvas","before":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]},"after":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"width":320,"data":{"title":"Image Panels"}},{"id":"media","type":"assetNode","position":{"x":400,"y":20},"data":{"url":"https://media.example/image.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}}],"edges":[{"id":"connection","source":"step","target":"media","sourceHandle":"out:image","targetHandle":"in:image"}]}}}},{"name":"Prepare one exact Canvas update","input":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"data":{"title":"Whiteboard"}},{"id":"note","type":"stickyNote","position":{"x":400,"y":20},"data":{"text":"Old"}}],"edges":[]},"updatedCanvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":40,"y":30},"data":{"title":"Whiteboard Step"}},{"id":"note","type":"stickyNote","position":{"x":440,"y":60},"data":{"text":"New","media":{"url":"https://media.example/keep.png"}}}],"edges":[]},"commit":true},"expected":{"canvas":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":40,"y":30},"data":{"title":"Whiteboard Step"}},{"id":"note","type":"stickyNote","position":{"x":440,"y":60},"data":{"text":"New","media":{"url":"https://media.example/keep.png"}}}],"edges":[]},"edit":{"schemaVersion":1,"title":"Update Canvas with 2 nodes","field":"canvas","before":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":0,"y":0},"data":{"title":"Whiteboard"}},{"id":"note","type":"stickyNote","position":{"x":400,"y":20},"data":{"text":"Old"}}],"edges":[]},"after":{"schemaVersion":1,"viewport":{"x":0,"y":0,"zoom":1},"nodes":[{"id":"step","type":"programmableBlockNode","position":{"x":40,"y":30},"data":{"title":"Whiteboard Step"}},{"id":"note","type":"stickyNote","position":{"x":440,"y":60},"data":{"text":"New","media":{"url":"https://media.example/keep.png"}}}],"edges":[]}}}},{"name":"Attach exact generated media versions","input":{"canvas":{"schemaVersion":1,"nodes":[],"edges":[]},"updatedCanvas":{"schemaVersion":1,"nodes":[],"edges":[]},"commit":false,"generatedImage":{"kind":"image","assetId":"image-asset","versionId":"image-version","url":"https://media.example/frame.png"},"addGeneratedImage":true,"generatedImageTitle":"Opening frame","generatedVideo":{"kind":"video","assetId":"video-asset","versionId":"video-version","url":"https://media.example/clip.mp4","mimeType":"video/mp4","width":1280,"height":720,"duration":6,"bytes":120000},"addGeneratedVideo":true,"generatedVideoTitle":"Opening motion","generatedAudio":{"kind":"audio","assetId":"audio-asset","versionId":"audio-version","url":"https://media.example/voice.wav","mimeType":"audio/wav","duration":12,"bytes":576044,"sampleRate":24000,"channels":1},"addGeneratedAudio":true,"generatedAudioTitle":"Main narration"},"expected":{"canvas":{"schemaVersion":1,"nodes":[{"id":"connected-image-image-asset-image-version","type":"imageGenNode","position":{"x":80,"y":80},"width":320,"height":320,"data":{"title":"Opening frame","assetId":"image-asset","versionId":"image-version","url":"https://media.example/frame.png","versions":[{"id":"image-version","url":"https://media.example/frame.png","assetId":"image-asset"}],"activeVersion":0,"selectedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-video-video-asset-video-version","type":"videoGenNode","position":{"x":104,"y":104},"width":360,"height":260,"data":{"title":"Opening motion","assetId":"video-asset","versionId":"video-version","url":"https://media.example/clip.mp4","duration":6,"versions":[{"id":"video-version","url":"https://media.example/clip.mp4","assetId":"video-asset","duration":6}],"currentVersion":0,"approvedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-audio-audio-asset-audio-version","type":"voiceoverGenNode","position":{"x":128,"y":128},"width":360,"height":260,"data":{"title":"Main narration","assetId":"audio-asset","versionId":"audio-version","url":"https://media.example/voice.wav","duration":12,"versions":[{"id":"audio-version","url":"https://media.example/voice.wav","assetId":"audio-asset","duration":12}],"currentVersion":0,"status":"complete","source":"connected-block"}}],"edges":[]},"edit":{"schemaVersion":1,"title":"Update Canvas with 3 nodes","field":"canvas","before":{"schemaVersion":1,"nodes":[],"edges":[]},"after":{"schemaVersion":1,"nodes":[{"id":"connected-image-image-asset-image-version","type":"imageGenNode","position":{"x":80,"y":80},"width":320,"height":320,"data":{"title":"Opening frame","assetId":"image-asset","versionId":"image-version","url":"https://media.example/frame.png","versions":[{"id":"image-version","url":"https://media.example/frame.png","assetId":"image-asset"}],"activeVersion":0,"selectedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-video-video-asset-video-version","type":"videoGenNode","position":{"x":104,"y":104},"width":360,"height":260,"data":{"title":"Opening motion","assetId":"video-asset","versionId":"video-version","url":"https://media.example/clip.mp4","duration":6,"versions":[{"id":"video-version","url":"https://media.example/clip.mp4","assetId":"video-asset","duration":6}],"currentVersion":0,"approvedVersion":0,"status":"complete","source":"connected-block"}},{"id":"connected-audio-audio-asset-audio-version","type":"voiceoverGenNode","position":{"x":128,"y":128},"width":360,"height":260,"data":{"title":"Main narration","assetId":"audio-asset","versionId":"audio-version","url":"https://media.example/voice.wav","duration":12,"versions":[{"id":"audio-version","url":"https://media.example/voice.wav","assetId":"audio-asset","duration":12}],"currentVersion":0,"status":"complete","source":"connected-block"}}],"edges":[]}}}}],"portable":{"format":"stillmade-block/1","dependencies":[],"sources":[{"kind":"original","license":"MIT","paths":["LICENSE","src/run.js","src/view.json"],"evidence":["upstream/0/LICENSE","upstream/0/src/run.js","upstream/0/src/view.json"],"changes":"Original implementation declared by the packager; no reused repository or dependency code."}],"files":{"upstream/0/LICENSE":"MIT License\n\nCopyright (c) 2026 StillMade contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","upstream/0/src/run.js":"const copy=value=>JSON.parse(JSON.stringify(value));\nconst source=copy(input.canvas||{schemaVersion:1,nodes:[],edges:[]});\nconst candidate=input.commit?copy(input.updatedCanvas):copy(source);\nif(!candidate||Array.isArray(candidate)||candidate.schemaVersion!==1||!Array.isArray(candidate.nodes)||!Array.isArray(candidate.edges))throw new Error('Keep the complete version 1 Canvas document with nodes and edges.');\nconst attach=(value,kind,title)=>{\n if(!value||value.kind!==kind)throw new Error('Connect an approved generated '+kind+' before adding it to Canvas.');\n const ref=copy(value),base='connected-'+kind+'-'+ref.assetId+'-'+ref.versionId;\n let id=base,suffix=2;while(candidate.nodes.some(node=>node.id===id)){id=base+'-'+suffix;suffix+=1}\n const position={x:80+candidate.nodes.length*24,y:80+candidate.nodes.length*24};\n if(kind==='image')candidate.nodes.push({id,type:'imageGenNode',position,width:320,height:320,data:{title:title||'Generated image',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId}],activeVersion:0,selectedVersion:0,status:'complete',source:'connected-block'}});\n if(kind==='video')candidate.nodes.push({id,type:'videoGenNode',position,width:360,height:260,data:{title:title||'Generated video',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,approvedVersion:0,status:'complete',source:'connected-block'}});\n if(kind==='audio')candidate.nodes.push({id,type:'voiceoverGenNode',position,width:360,height:260,data:{title:title||'Voiceover',assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,status:'complete',source:'connected-block'}});\n};\nif(input.addGeneratedImage)attach(input.generatedImage,'image',input.generatedImageTitle);\nif(input.addGeneratedVideo)attach(input.generatedVideo,'video',input.generatedVideoTitle);\nif(input.addGeneratedAudio)attach(input.generatedAudio,'audio',input.generatedAudioTitle);\nif(candidate.nodes.length>5000||candidate.edges.length>10000)throw new Error('Canvas supports up to 5,000 nodes and 10,000 connections.');\nif(!candidate.nodes.every(node=>node&&typeof node==='object'&&!Array.isArray(node)&&typeof node.id==='string'&&node.id&&typeof node.type==='string'&&node.type&&node.position&&Number.isFinite(node.position.x)&&Number.isFinite(node.position.y)))throw new Error('Every Canvas node needs an identity, type, and finite position.');\nif(new Set(candidate.nodes.map(node=>node.id)).size!==candidate.nodes.length)throw new Error('Canvas node identities must be distinct.');\nconst ids=new Set(candidate.nodes.map(node=>node.id));\nif(!candidate.edges.every(edge=>edge&&typeof edge==='object'&&!Array.isArray(edge)&&typeof edge.id==='string'&&edge.id&&ids.has(edge.source)&&ids.has(edge.target)))throw new Error('Every Canvas connection needs an identity and existing source and target nodes.');\nif(new Set(candidate.edges.map(edge=>edge.id)).size!==candidate.edges.length)throw new Error('Canvas connection identities must be distinct.');\nreturn{canvas:candidate,edit:{schemaVersion:1,title:`Update Canvas with ${candidate.nodes.length} node${candidate.nodes.length===1?'':'s'}`,field:'canvas',before:source,after:candidate}};\n","upstream/0/src/view.json":"{\n  \"html\": \"<main class=\\\"canvas-workspace\\\">\\n  <header><div><small>Production workspace</small><h1>Canvas</h1><p id=\\\"summary\\\">Loading the complete Canvas…</p></div><div class=\\\"actions\\\"><button id=\\\"reset\\\" type=\\\"button\\\" class=\\\"secondary\\\">Reset changes</button><button id=\\\"add\\\" type=\\\"button\\\" class=\\\"secondary\\\">Add note</button><button id=\\\"save\\\" type=\\\"button\\\">Review changes</button></div></header>\\n  <output id=\\\"status\\\" role=\\\"status\\\" aria-live=\\\"polite\\\"></output>\\n  <section><div class=\\\"section-title\\\"><h2>Connected media</h2></div><div class=\\\"handoffs\\\"><div id=\\\"handoff-image\\\"></div><div id=\\\"handoff-video\\\"></div><div id=\\\"handoff-audio\\\"></div></div></section>\\n  <section><h2>Canvas items</h2><p class=\\\"hint\\\">Project Type Steps stay connected and carry their exact installed source. Edit their placement or label here; use the Project Type builder to add, remove, replace, or reconnect Steps.</p><div id=\\\"nodes\\\" class=\\\"cards\\\"></div></section>\\n  <section><h2>Connections</h2><div id=\\\"edges\\\" class=\\\"connections\\\"></div></section>\\n  <details><summary>Complete Canvas data</summary><p>Native content, media references, dimensions, and custom fields are retained. Executable source and host bindings are protected when StillMade applies the proposal.</p><textarea id=\\\"raw\\\" class=\\\"raw\\\" spellcheck=\\\"false\\\"></textarea><button id=\\\"applyRaw\\\" type=\\\"button\\\" class=\\\"secondary\\\">Apply JSON to workspace</button></details>\\n</main>\\n\",\n  \"css\": \".canvas-workspace{max-width:1120px;margin:0 auto;padding:8px;color:var(--fg)}header,.actions,.card-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}header{justify-content:space-between;margin-bottom:14px}header h1{margin:2px 0}header p,header small,details p,output,.hint{color:var(--fg-muted)}section,details{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3);background:var(--bg-elev-1)}section h2{margin:0 0 8px}.cards,.connections{display:grid;gap:10px}.handoffs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.handoff,.card,.connection{padding:12px;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg)}.handoff p{min-height:38px}.card-head{justify-content:space-between}.card h3{margin:0;font-size:16px}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}label{display:grid;gap:5px;margin:7px 0;font-size:13px;font-weight:600}input,textarea{width:100%;background:var(--bg)}textarea{min-height:76px;resize:vertical}.wide{grid-column:1/-1}.secondary{background:transparent;color:var(--fg)}.danger{background:transparent;color:var(--signal)}.raw{min-height:420px;font-family:var(--font-mono);font-size:12px}output{display:block;min-height:24px;margin:8px 2px}.badge{font-size:12px;color:var(--fg-muted)}\\n@media(max-width:760px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.canvas-workspace{padding:0}.grid,.handoffs{grid-template-columns:1fr}.actions{width:100%}.actions button{flex:1}section,details{padding:12px}}\\n\",\n  \"javascript\": \"const status=document.getElementById('status'),summary=document.getElementById('summary'),raw=document.getElementById('raw'),save=document.getElementById('save');\\nlet source={schemaVersion:1,nodes:[],edges:[]},draft={schemaVersion:1,nodes:[],edges:[]},connectedImage=null,connectedVideo=null,connectedAudio=null,titles={image:'Generated image',video:'Generated video',audio:'Voiceover'},busy=false,revision=0;\\nconst clone=value=>JSON.parse(JSON.stringify(value));\\nconst text=value=>value===undefined||value===null?'':String(value);\\nconst managed=node=>node.type==='programmableBlockNode'||!!(node.data&&node.data.hostBlock);\\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},{tag:'input',type,action,value:Number.isFinite(value)?value:text(value),disabled:busy,label}]});\\nconst nodeCard=(node,index)=>({tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h3',text:text(node.data&&node.data.title)||text(node.data&&node.data.label)||text(node.data&&node.data.name)||text(node.id)},{tag:'span',className:'badge',text:managed(node)?'Project Type Step':text(node.type)},{tag:'button',action:'remove:'+String(index),text:'Remove',className:'danger',disabled:busy||managed(node)}]},{tag:'div',className:'grid',children:[field('Title','title:'+String(index),node.data&&node.data.title),field('Label','label:'+String(index),node.data&&node.data.label),field('X','x:'+String(index),node.position&&node.position.x,'number'),field('Y','y:'+String(index),node.position&&node.position.y,'number'),field('Width','width:'+String(index),node.width,'number'),field('Height','height:'+String(index),node.height,'number'),managed(node)?{tag:'p',className:'wide hint',text:'Its executable source and connections are preserved.'}:field('Content','content:'+String(index),node.data&&(node.data.text===undefined?node.data.content:node.data.text),'text',true)]}]});\\nconst edgeCard=edge=>({tag:'article',className:'connection',children:[{tag:'strong',text:text(edge.source)+' → '+text(edge.target)},{tag:'p',className:'hint',text:text(edge.sourceHandle)+' '+text(edge.targetHandle)}]});\\nconst ensure=()=>{if(!draft||Array.isArray(draft)||draft.schemaVersion!==1)draft=clone(source);if(!Array.isArray(draft.nodes))draft.nodes=[];if(!Array.isArray(draft.edges))draft.edges=[]};\\nconst handoff=(kind,value,label)=>value?[{tag:'article',className:'handoff',children:[{tag:'strong',text:label},{tag:'p',className:'hint',text:text(value.assetId)+' · version '+text(value.versionId)},{tag:'button',action:'attach-'+kind,text:'Add '+kind+' to Canvas',className:'secondary',disabled:busy}]}]:[{tag:'article',className:'handoff',children:[{tag:'strong',text:label},{tag:'p',className:'hint',text:'Connect a stored '+kind+' output to add it with exact asset and version identity.'}]}];\\nconst draw=()=>{ensure();StillMade.render('handoff-image',handoff('image',connectedImage,'Image'));StillMade.render('handoff-video',handoff('video',connectedVideo,'Video'));StillMade.render('handoff-audio',handoff('audio',connectedAudio,'Narration'));StillMade.render('nodes',draft.nodes.length?draft.nodes.map(nodeCard):[{tag:'p',text:'No Canvas items yet.'}]);StillMade.render('edges',draft.edges.length?draft.edges.map(edgeCard):[{tag:'p',text:'No connections.'}]);raw.value=JSON.stringify(draft,null,2);summary.textContent=String(draft.nodes.length)+' item'+(draft.nodes.length===1?'':'s')+' · '+String(draft.edges.length)+' connection'+(draft.edges.length===1?'':'s');save.disabled=busy;document.getElementById('reset').disabled=busy;document.getElementById('add').disabled=busy};\\nconst changed=()=>{revision+=1;raw.value=JSON.stringify(draft,null,2);status.textContent='Changes are local until you choose Review changes.'};\\nconst attach=(kind,ref,title)=>{const id='connected-'+kind+'-'+ref.assetId+'-'+ref.versionId+'-'+String(revision+1),position={x:80+draft.nodes.length*24,y:80+draft.nodes.length*24};let node;if(kind==='image')node={id,type:'imageGenNode',position,width:320,height:320,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId}],activeVersion:0,selectedVersion:0,status:'complete',source:'connected-block'}};if(kind==='video')node={id,type:'videoGenNode',position,width:360,height:260,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,approvedVersion:0,status:'complete',source:'connected-block'}};if(kind==='audio')node={id,type:'voiceoverGenNode',position,width:360,height:260,data:{title,assetId:ref.assetId,versionId:ref.versionId,url:ref.url,duration:ref.duration,versions:[{id:ref.versionId,url:ref.url,assetId:ref.assetId,duration:ref.duration}],currentVersion:0,status:'complete',source:'connected-block'}};draft.nodes=[...draft.nodes,node];changed();draw();status.textContent='Connected '+kind+' added with its exact asset and version identity.'};\\nStillMade.onAction(event=>{if(busy)return;ensure();const parts=event.action.split(':'),kind=parts[0],index=Number(parts[1]);if(kind==='attach-image'&&connectedImage){attach('image',connectedImage,titles.image);return}if(kind==='attach-video'&&connectedVideo){attach('video',connectedVideo,titles.video);return}if(kind==='attach-audio'&&connectedAudio){attach('audio',connectedAudio,titles.audio);return}const node=draft.nodes.find((item,position)=>position===index);if(!node)return;if(kind==='remove'&&!managed(node)){const id=node.id;draft.nodes=draft.nodes.filter((item,position)=>position!==index);draft.edges=draft.edges.filter(edge=>edge.source!==id&&edge.target!==id);changed();draw();return}if(!node.data||Array.isArray(node.data))node.data={};if(kind==='title')node.data.title=event.value;if(kind==='label')node.data.label=event.value;if(kind==='x')node.position.x=Number(event.value)||0;if(kind==='y')node.position.y=Number(event.value)||0;if(kind==='width')node.width=Math.max(1,Number(event.value)||1);if(kind==='height')node.height=Math.max(1,Number(event.value)||1);if(kind==='content'&&!managed(node)){if(node.data.text!==undefined)node.data.text=event.value;else node.data.content=event.value}changed()});\\ndocument.getElementById('add').addEventListener('click',()=>{if(busy)return;ensure();const id='portable-note-'+String(draft.nodes.length+1)+'-'+String(revision+1);draft.nodes=[...draft.nodes,{id,type:'stickyNote',position:{x:80+draft.nodes.length*24,y:80+draft.nodes.length*24},width:260,height:180,data:{title:'Note',text:'New note'}}];changed();draw()});\\ndocument.getElementById('applyRaw').addEventListener('click',()=>{if(busy)return;try{const value=JSON.parse(raw.value);if(!value||Array.isArray(value)||value.schemaVersion!==1||!Array.isArray(value.nodes)||!Array.isArray(value.edges))throw Error('Canvas JSON must be a version 1 document with nodes and edges.');draft=clone(value);changed();draw();status.textContent='Complete Canvas data applied locally.'}catch(error){status.textContent=error.message}});\\ndocument.getElementById('reset').addEventListener('click',()=>{if(!busy){draft=clone(source);revision+=1;draw();status.textContent='Changes reset to the current Canvas.'}});\\nsave.addEventListener('click',async()=>{if(busy)return;const started=revision;busy=true;draw();status.textContent='Checking the complete Canvas…';try{const result=await StillMade.run({canvas:source,updatedCanvas:draft,commit:true});draft=clone(result.canvas);if(started===revision)status.textContent='Canvas changes are ready below. Review the before and after, then choose Apply.'}catch(error){status.textContent=error.message||'Canvas changes could not be prepared.'}finally{busy=false;draw()}});\\nStillMade.onInput(values=>{source=clone(values.canvas||source);draft=clone(source);connectedImage=values.generatedImage?clone(values.generatedImage):null;connectedVideo=values.generatedVideo?clone(values.generatedVideo):null;connectedAudio=values.generatedAudio?clone(values.generatedAudio):null;titles={image:values.generatedImageTitle||'Generated image',video:values.generatedVideoTitle||'Generated video',audio:values.generatedAudioTitle||'Voiceover'};revision+=1;draw();status.textContent='Edit the complete Canvas. Project Type Steps remain protected as whole Blocks.'});\\n\"\n}"},"lineage":{"parents":[],"changes":"Packaged original Block source for StillMade."}}}],"license":"MIT"}