{"manifest":{"schemaVersion":1,"sdkVersion":"0.1.0","id":"sm.board-editor","version":"2.2.7","name":"Whiteboard","description":"Create and arrange Whiteboards with connected exact media, spatial preview, timed playback, presenter actions, and Editor output with bounded saved revision and collaborator context.","kind":"workspace","runtime":"javascript","entry":"src/run.js","license":"MIT","provenance":{"notice":"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"},"inputs":{"board":{"type":"board-document","context":"board","primary":true,"required":false,"default":{"schemaVersion":1,"strip":[]},"description":"The complete current Whiteboard document. Existing text, media references, motion, timing, and custom fields are retained."},"updatedBoard":{"type":"board-document","required":false,"default":{"schemaVersion":1,"strip":[]},"description":"The complete edited Whiteboard document prepared in this workspace."},"commit":{"type":"boolean","default":false,"description":"Prepare the edited Whiteboard for StillMade review and Apply."},"action":{"type":"text","required":false,"default":"none","description":"The Block-owned interface selects a declared host action."},"presenterPrompt":{"type":"text","required":false,"default":"Friendly presenter explaining the narration","description":"Presenter direction sent only after approval."},"generatedImage":{"type":"image","imageMode":"reference","required":false,"description":"An exact stored image from an approved generation or import Block, available for placement on the selected board."},"versions":{"type":"metadata[]","context":"versions","required":false,"description":"Host-backed saved revision records for this exact project workspace."},"presence":{"type":"metadata[]","required":false,"description":"Bounded collaborator presence supplied through a declared connection or host project context."}},"outputs":{"board":{"type":"board-document","primary":true,"role":"whiteboard_document","description":"The complete edited Whiteboard document for the next Step."},"edit":{"type":"workspace-edit","description":"An exact before/after Whiteboard change reviewed before it updates the project."},"presenterAction":{"type":"approval","required":false,"role":"portable_media_action","description":"Generate presenter poses and return a reviewable Whiteboard edit."},"qualityAction":{"type":"approval","required":false,"role":"portable_media_action","description":"Analyze exact saved images."},"renderAction":{"type":"approval","required":false,"role":"portable_media_action","description":"Render the current board sequence."},"actionEdit":{"type":"workspace-edit","required":false,"description":"Reviewable presenter placement result."},"qualityReport":{"type":"metadata","required":false},"editorTimeline":{"type":"timeline","role":"editor_handoff","description":"An editable timeline preserving every canvas board for a downstream Editor Step."},"revision":{"type":"metadata","description":"A deterministic revision event for durable host storage and collaboration propagation.","required":false}},"permissions":{"project":["context.board.read","workspace.board.propose","context.versions.read"],"network":[],"filesystem":[],"secrets":[],"actions":["presenter.generate","quality.analyze","final.render"]},"ui":[],"platforms":{"phone":{"supported":true},"browser":{"supported":true},"desktop":{"supported":true}}},"code":"const copy = value => JSON.parse(JSON.stringify(value));\nconst source = copy(input.board || {schemaVersion: 1, strip: []});\nconst candidate = input.commit ? copy(input.updatedBoard) : copy(source);\nif (!candidate || Array.isArray(candidate) || candidate.schemaVersion !== 1 || !Array.isArray(candidate.strip)) {\n  throw new Error('Keep the complete version 1 Whiteboard document and its strip.');\n}\nif (candidate.strip.length > 2000 || !candidate.strip.every(board => board && typeof board === 'object' && !Array.isArray(board))) {\n  throw new Error('Whiteboard supports up to 2,000 valid board rows.');\n}\nconst canvasBoards = candidate.strip.filter(board => board.type === 'canvas');\nif (!canvasBoards.every(board => !board.elements || Array.isArray(board.elements) && board.elements.length <= 5000 && board.elements.every(element => element && typeof element === 'object' && !Array.isArray(element)))) {\n  throw new Error('Each board needs a valid element list with up to 5,000 elements.');\n}\nconst itemEnd=item=>Math.max(0,Number(item.start)||0)+Math.max(.001,Number(item.duration)||Number(item.durationMs)/1000||.001);\nconst clips=candidate.strip.filter(board=>board.type==='canvas').map((board,index)=>({id:board.panelId||'board-'+index,start:(Number(board.startMs)||0)/1000,duration:Math.max(.001,(Number(board.durationMs)||1000)/1000),canvas:copy(board)}));\nconst targetDuration=Math.max(.001,...clips.map(itemEnd));\nconst editorTimeline={schemaVersion:1,activeTimelineId:'whiteboard',clips,audioItems:Array.isArray(candidate.audioItems)?copy(candidate.audioItems):[],captions:Array.isArray(candidate.captions)?copy(candidate.captions):[],textOverlays:[],effects:[],targetDuration};\nconst outputs={board:candidate,editorTimeline,edit:{schemaVersion:1,title:`Update Whiteboard (${candidate.strip.length} boards)`,field:'board',before:source,after:candidate}};if(Array.isArray(input.versions)||Array.isArray(input.presence))outputs.revision={schemaVersion:1,scope:'board',baseRevision:(Array.isArray(input.versions)?input.versions:[]).slice(0,200).reduce((highest,item)=>Math.max(highest,Number(item&&item.revision)||0),0),committed:!!input.commit,changed:JSON.stringify(source)!==JSON.stringify(candidate),paths:JSON.stringify(source)===JSON.stringify(candidate)?[]:['board']};const images=candidate.strip.flatMap(board=>board.elements||[]).filter(item=>item&&item.kind==='image'&&item.assetId&&item.versionId&&(item.url||item.src)).slice(0,16);\nif(input.action==='presenter')outputs.presenterAction={schemaVersion:1,kind:'stillmade.media-action',operation:'presenter.generate',request:{board:candidate,prompt:input.presenterPrompt,voiceover:candidate.audio||{schemaVersion:1,words:[]}},resultPort:'actionEdit'};\nif(input.action==='quality'&&images.length)outputs.qualityAction={schemaVersion:1,kind:'stillmade.media-action',operation:'quality.analyze',request:{assets:images.map(image=>({kind:'image',assetId:image.assetId,versionId:image.versionId,url:image.url||image.src}))},resultPort:'qualityReport'};\nif(input.action==='render')outputs.renderAction={schemaVersion:1,kind:'stillmade.media-action',operation:'final.render',request:{schemaVersion:1,kind:'video',timeline:editorTimeline,settings:{fps:30,resolution:1080,quality:'high'},source:{id:'sm.board-editor',version:'2.2.7'}},resultPort:null};\nreturn outputs;\n","view":{"html":"<main class=\"board-workspace\">\n  <header><div><small>Production workspace</small><h1>Whiteboard</h1><p id=\"summary\">Loading the Whiteboard…</p></div><div class=\"actions\"><button id=\"reset\" type=\"button\" class=\"secondary\">Reset changes</button><button id=\"add\" type=\"button\" class=\"secondary\">Add board</button><button id=\"save\" type=\"button\" data-stillmade-action=\"run\">Review changes</button></div></header>\n  <output id=\"status\" role=\"status\" aria-live=\"polite\"></output>\n  <section><h2>Saved revisions and collaborators</h2><div id=\"workspaceSync\"></div></section>\n  <section><div class=\"card-head\"><div><h2>Connected media</h2><p id=\"connectedMedia\">Connect Generate image or Import media to place an exact stored image.</p></div><div class=\"actions\"><button id=\"addConnectedImage\" type=\"button\" class=\"secondary\">Add connected image</button><button id=\"previewSelected\" type=\"button\" class=\"secondary\">Preview selected image</button></div></div><img id=\"mediaPreview\" alt=\"Selected Whiteboard image preview\" hidden></section>\n  <section class=\"settings\"><h2>Workspace settings</h2><label>Aspect ratio<input id=\"aspect\" type=\"text\" placeholder=\"16:9\"></label></section>\n  <section class=\"preview\"><div class=\"card-head\"><div><h2>Sequence preview</h2><p>Every preview frame becomes an editable Editor clip.</p></div><button id=\"prepareEditor\" type=\"button\" class=\"secondary\">Prepare Editor handoff</button></div><div class=\"playback\"><button id=\"advance\" type=\"button\" class=\"secondary\">Advance 0.5s</button><label for=\"playhead\">Preview position</label><input id=\"playhead\" type=\"range\" min=\"0\" max=\"1\" step=\"100\" value=\"0\"><output id=\"playTime\" for=\"playhead\">0.0s</output></div><div id=\"sequencePreview\" class=\"preview-strip\" aria-label=\"Whiteboard sequence preview\"></div></section>\n  <section id=\"boards\" class=\"boards\" aria-label=\"Whiteboard boards\"></section>\n  <details><summary>Complete Whiteboard data</summary><p>Every field and placed media reference is retained. Use this editor for custom fields not shown on the board cards.</p><label for=\"raw\">Complete Whiteboard JSON</label><textarea id=\"raw\" class=\"raw\" spellcheck=\"false\"></textarea><button id=\"applyRaw\" type=\"button\" class=\"secondary\">Apply JSON to workspace</button></details>\n<section><h2>Presenter and delivery</h2><label>Presenter direction<input id=\"presenterPrompt\" type=\"text\" value=\"Friendly presenter explaining the narration\"></label><div class=\"actions\"><button id=\"generatePresenter\" type=\"button\" class=\"secondary\">Generate presenter poses</button><button id=\"qualityReview\" type=\"button\" class=\"secondary\">Review image quality</button><button id=\"renderFinal\" type=\"button\">Render final video</button></div></section></main>\n","css":".board-workspace{max-width:1120px;margin:0 auto;padding:8px;color:var(--fg)}header,.actions,.card-head,.row{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{color:var(--fg-muted)}\n.settings,.preview,.card,details{margin:12px 0;padding:16px;border:1px solid var(--border-tok);border-radius:var(--r-3);background:var(--bg-elev-1)}.settings h2,.card h2{margin:0;font-size:18px}.boards{display:grid;gap:12px}.playback{display:grid;grid-template-columns:auto auto minmax(140px,1fr) auto;align-items:center;gap:10px;margin:10px 0}.playback label{margin:0}.preview-strip{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(150px,1fr);gap:10px;overflow-x:auto;padding:8px 0}.preview-frame{position:relative;display:grid;gap:6px;min-height:110px;padding:14px;border:1px solid var(--border-strong);border-radius:var(--r-2);background:var(--bg);align-content:center}.preview-frame span{color:var(--fg-muted)}.preview-frame.active{outline:3px solid var(--accent);outline-offset:2px}.spatial-stage{position:relative;min-height:150px;overflow:hidden;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg-elev-1)}.spatial-item{position:absolute;min-width:44px;min-height:44px;max-width:70%;padding:6px;border:1px solid var(--border-strong);border-radius:var(--r-2);background:var(--bg);overflow:hidden;text-overflow:ellipsis}.spatial-item.selected{outline:2px solid var(--accent)}.x-0{left:0%}.x-1{left:9%}.x-2{left:18%}.x-3{left:27%}.x-4{left:36%}.x-5{left:45%}.x-6{left:54%}.x-7{left:63%}.x-8{left:72%}.x-9{left:81%}.x-10{left:90%}.y-0{top:0%}.y-1{top:8%}.y-2{top:16%}.y-3{top:24%}.y-4{top:32%}.y-5{top:40%}.y-6{top:48%}.y-7{top:56%}.y-8{top:64%}.y-9{top:72%}.y-10{top:80%}.size-s{width:14%}.size-m{width:24%}.size-l{width:38%}.card-head{justify-content:space-between}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.elements{display:grid;gap:8px;margin-top:10px}.element{padding:10px;border:1px solid var(--border-tok);border-radius:var(--r-2);background:var(--bg)}.retained-row{border-style:dashed}label{display:grid;gap:5px;margin:8px 0;font-size:13px;font-weight:600}input,textarea,select{width:100%;background:var(--bg)}button,input,textarea,select,summary{min-height:44px}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.55}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:360px;font-family:var(--font-mono);font-size:12px}output{display:block;min-height:24px;margin:8px 2px}\n@media(max-width:820px){.playback{grid-template-columns:auto 1fr}.playback label{grid-column:1/-1}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:560px){.board-workspace{padding:0}.grid{grid-template-columns:1fr}.actions{width:100%}.actions button{flex:1}.settings,.preview,.card,details{padding:12px}}\n","javascript":"const status=document.getElementById('status'),summary=document.getElementById('summary'),raw=document.getElementById('raw'),save=document.getElementById('save'),aspect=document.getElementById('aspect');\nlet source={schemaVersion:1,strip:[]},draft={schemaVersion:1,strip:[]},busy=false,revision=0,playheadMs=0,selectedElement='',connectedImage=null;\nconst clone=value=>JSON.parse(JSON.stringify(value));\nconst text=value=>value===undefined||value===null?'':String(value);\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},type==='textarea'?{tag:'textarea',action,value:text(value),disabled:busy,label}:{tag:'input',type,action,value:Number.isFinite(value)?value:text(value),disabled:busy,label}]});\nconst choice=(label,action,value,options)=>({tag:'label',children:[{tag:'span',text:label},{tag:'select',action,value:text(value),disabled:busy,label,children:options.map(option=>({tag:'option',text:option,value:option}))}]});\nconst toggle=(label,action,value)=>({tag:'label',children:[{tag:'span',text:label},{tag:'input',type:'checkbox',action,checked:!!value,disabled:busy,label}]});\nconst optionList=(value,options)=>options.includes(text(value))||!text(value)?options:[...options,text(value)];\nconst moveButton=(action,label,disabled)=>({tag:'button',action,text:label,className:'secondary',disabled:busy||disabled});\nconst elementCard=(element,boardIndex,elementIndex)=>{const key=String(boardIndex)+':'+String(elementIndex),kind=text(element.kind)||'text',isText=kind==='text',isMedia=kind==='image'||kind==='video',owner=draft.strip.find((item,position)=>position===boardIndex);return {tag:'article',className:'element',children:[{tag:'div',className:'card-head',children:[{tag:'strong',text:kind+' · '+(text(element.text)||text(element.descriptor)||text(element.query)||text(element.id)||String(elementIndex+1))},{tag:'div',className:'row',children:[moveButton('element-up:'+key,'Up',elementIndex===0),moveButton('element-down:'+key,'Down',elementIndex===(owner.elements||[]).length-1),{tag:'button',action:'duplicate-element:'+key,text:'Duplicate',className:'secondary',disabled:busy},{tag:'button',action:'remove-element:'+key,text:'Remove',className:'danger',disabled:busy}]}]},{tag:'div',className:'grid',children:[field('Element ID','element-id:'+key,element.id),choice('Kind','element-kind:'+key,kind,optionList(kind,['text','image','video'])),field('Appears (ms)','element-appear:'+key,element.appearMs,'number'),field('Leaves (ms)','element-leave:'+key,element.leaveMs,'number'),field('Size','element-size:'+key,element.size,'number'),field('X position','element-x:'+key,element.pos&&element.pos.x,'number'),field('Y position','element-y:'+key,element.pos&&element.pos.y,'number'),toggle('Pinned position','element-pinned:'+key,element.pinned),choice('Entrance','element-entrance:'+key,element.entrance||element.anim&&element.anim.type||'pop',optionList(element.entrance||element.anim&&element.anim.type,['pop','fade','draw','slide','none'])),field('Sound effect','element-sfx:'+key,element.sfx),...(isText?[field('Text','element-text:'+key,element.text,'textarea',true),field('Font','element-font:'+key,element.font),field('Color','element-color:'+key,element.color),field('Weight','element-weight:'+key,element.weight,'number'),toggle('Reveal per word','element-per-word:'+key,element.anim&&element.anim.perWord),field('Animation speed (ms)','element-speed:'+key,element.anim&&element.anim.speedMs,'number')]:[]),...(isMedia?[field('Description','element-descriptor:'+key,element.descriptor,'textarea',true),field('Asset ID','element-asset:'+key,element.assetId),field('Version ID','element-version:'+key,element.versionId),field('Media source','element-src:'+key,element.src,'textarea',true)]:[]),...(kind==='video'?[field('Source in (ms)','element-source-in:'+key,element.sourceInMs,'number'),field('Source out (ms)','element-source-out:'+key,element.sourceOutMs,'number'),toggle('Muted','element-muted:'+key,element.muted),toggle('Loop','element-loop:'+key,element.loop)]:[])]}]}};\nconst canvasPositions=()=>draft.strip.map((board,index)=>board.type==='canvas'?index:-1).filter(index=>index>=0);\nconst boardCard=(board,index)=>{if(board.type!=='canvas')return {tag:'article',className:'card retained-row',children:[{tag:'h2',text:'Retained '+(text(board.type)||'workspace')+' row'},{tag:'p',text:'This non-board row stays in its exact strip position and is available in Complete Whiteboard data.'}]};const positions=canvasPositions(),position=positions.indexOf(index);return {tag:'article',className:'card',children:[{tag:'div',className:'card-head',children:[{tag:'h2',text:'Board '+String(position+1)+' · '+(text(board.panelId)||'untitled')},{tag:'div',className:'row',children:[moveButton('move-up:'+String(index),'Up',position===0),moveButton('move-down:'+String(index),'Down',position===positions.length-1),{tag:'button',action:'add-text:'+String(index),text:'Add text',className:'secondary',disabled:busy},{tag:'button',action:'remove-board:'+String(index),text:'Remove board',className:'danger',disabled:busy||positions.length<=1}]}]},{tag:'div',className:'grid',children:[field('Board ID','board-id:'+String(index),board.panelId),field('Scene ID','board-scene:'+String(index),board.sceneId),field('Background','board-background:'+String(index),board.background),field('Starts (ms)','board-start:'+String(index),board.startMs,'number'),field('Length (ms)','board-duration:'+String(index),board.durationMs,'number'),choice('Transition','board-transition:'+String(index),board.transitionIn||'cut',optionList(board.transitionIn,['cut','fade','swirl','wipe','slide'])),field('Transition length (ms)','board-transition-ms:'+String(index),board.transitionMs,'number'),toggle('Automatic arrangement','board-arrange:'+String(index),board.autoArrange!==false)]},{tag:'div',className:'elements',children:Array.isArray(board.elements)&&board.elements.length?board.elements.map((element,elementIndex)=>elementCard(element,index,elementIndex)):[{tag:'p',text:'No elements yet. Add text, request a presenter, or use Complete Whiteboard data for retained media.'}]}]}};\nconst ensure=()=>{if(!draft||Array.isArray(draft)||draft.schemaVersion!==1)draft={schemaVersion:1,strip:[]};if(!Array.isArray(draft.strip))draft.strip=[];if(!draft.meta||Array.isArray(draft.meta))draft.meta={}};\nconst renumber=()=>{let start=0;draft.strip=draft.strip.map(board=>{if(board.type!=='canvas')return board;const next={...board,startMs:start};start+=Math.max(0,Number(board.durationMs)||0);return next})};\nconst draw=()=>{ensure();StillMade.render('sequencePreview',draft.strip.map((board,boardIndex)=>({board,boardIndex})).filter(item=>item.board.type==='canvas').slice(0,30).map(({board,boardIndex},index)=>({tag:'article',id:'preview-frame-'+String(boardIndex),className:'preview-frame',children:[{tag:'strong',text:'Board '+String(index+1)},{tag:'span',text:text(board.panelId)||'Untitled'},{tag:'span',text:String((board.elements||[]).length)+' elements · '+String(Math.max(1,Number(board.durationMs)||1))+' ms'},{tag:'div',className:'spatial-stage',children:(board.elements||[]).slice(0,20).map((element,elementIndex)=>{const x=Math.round(Math.max(0,Math.min(1,Number(element.pos&&element.pos.x)||.5))*10),y=Math.round(Math.max(0,Math.min(1,Number(element.pos&&element.pos.y)||.5))*10),size=Number(element.size)||.18,sizeClass=size<.14?' size-s':size>.3?' size-l':' size-m';return {tag:'button',id:'preview-item-'+String(boardIndex)+'-'+String(elementIndex),className:'spatial-item x-'+String(x)+' y-'+String(y)+sizeClass+(selectedElement===String(boardIndex)+':'+String(elementIndex)?' selected':''),action:'select-element:'+String(boardIndex)+':'+String(elementIndex),text:text(element.text||element.descriptor||element.id||element.kind||'Element')}})}]})));updatePlayback();StillMade.render('boards',draft.strip.length?draft.strip.map(boardCard):[{tag:'p',text:'No boards yet. Add one to begin.'}]);raw.value=JSON.stringify(draft,null,2);aspect.value=text(draft.meta.aspect);const count=draft.strip.filter(board=>board.type==='canvas').length;summary.textContent=String(count)+' board'+(count===1?'':'s')+' in this complete Whiteboard';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.'};\naspect.addEventListener('input',()=>{ensure();draft.meta.aspect=aspect.value;changed()});\nStillMade.onAction(event=>{if(busy)return;ensure();const parts=event.action.split(':'),kind=parts[0],boardIndex=Number(parts[1]),elementIndex=Number(parts[2]),board=draft.strip.find((item,position)=>position===boardIndex),element=board&&Array.isArray(board.elements)?board.elements.find((item,position)=>position===elementIndex):null;\n const positions=canvasPositions(),canvasPosition=positions.indexOf(boardIndex),wantedPosition=kind==='move-up'?canvasPosition-1:kind==='move-down'?canvasPosition+1:-1,moveTarget=positions.find((value,position)=>position===wantedPosition);\n if(kind==='select-element'&&element){selectedElement=String(boardIndex)+':'+String(elementIndex);draw();status.textContent='Selected '+text(element.text||element.descriptor||element.id||element.kind||'element')+' in the spatial preview.';return}if(kind==='remove-board'&&positions.length>1){draft.strip=draft.strip.filter((item,position)=>position!==boardIndex);renumber();changed();draw();return}if(moveTarget!==undefined&&board){const target=draft.strip.find((item,position)=>position===moveTarget);draft.strip=draft.strip.map((item,position)=>position===moveTarget?board:position===boardIndex?target:item);renumber();changed();draw();return}if(!board||board.type!=='canvas')return;\n if(kind==='add-text'){if(!Array.isArray(board.elements))board.elements=[];board.elements=[...board.elements,{id:text(board.panelId)+'-text-'+String(board.elements.length+1)+'-'+String(revision+1),kind:'text',text:'New text',appearMs:0,leaveMs:null,pos:null,size:0.08,font:'Hanken Grotesk',color:'#111111',weight:800,anim:{type:'pop',perWord:false,speedMs:180},pinned:false}];changed();draw();return}if(kind==='remove-element'&&element){board.elements=board.elements.filter((item,position)=>position!==elementIndex);changed();draw();return}if(kind==='duplicate-element'&&element){const copy=clone(element);copy.id=text(board.panelId)+'-'+text(element.kind||'element')+'-'+String(board.elements.length+1)+'-'+String(revision+1);board.elements=[...board.elements.slice(0,elementIndex+1),copy,...board.elements.slice(elementIndex+1)];changed();draw();return}if((kind==='element-up'||kind==='element-down')&&element){const targetIndex=kind==='element-up'?elementIndex-1:elementIndex+1,target=board.elements.find((candidate,candidatePosition)=>candidatePosition===targetIndex);if(target)board.elements=board.elements.map((item,position)=>position===targetIndex?element:position===elementIndex?target:item);changed();draw();return}\n if(kind==='board-id')board.panelId=event.value;if(kind==='board-scene')board.sceneId=event.value||null;if(kind==='board-background')board.background=event.value;if(kind==='board-start')board.startMs=Math.max(0,Number(event.value)||0);if(kind==='board-duration'){board.durationMs=Math.max(1,Number(event.value)||1);renumber()}if(kind==='board-transition')board.transitionIn=event.value;if(kind==='board-transition-ms')board.transitionMs=Math.max(0,Number(event.value)||0);if(kind==='board-arrange')board.autoArrange=event.checked;\n if(element){if(kind==='element-id')element.id=event.value;if(kind==='element-kind')element.kind=event.value;if(kind==='element-text')element.text=event.value;if(kind==='element-descriptor')element.descriptor=event.value;if(kind==='element-asset')element.assetId=event.value||null;if(kind==='element-version')element.versionId=event.value||null;if(kind==='element-src')element.src=event.value||null;if(kind==='element-appear')element.appearMs=Math.max(0,Number(event.value)||0);if(kind==='element-leave')element.leaveMs=event.value===''?null:Math.max(0,Number(event.value)||0);if(kind==='element-size')element.size=Math.max(.01,Number(event.value)||.01);if(kind==='element-x'){if(!element.pos||Array.isArray(element.pos))element.pos={x:0,y:0};element.pos.x=Number(event.value)||0}if(kind==='element-y'){if(!element.pos||Array.isArray(element.pos))element.pos={x:0,y:0};element.pos.y=Number(event.value)||0}if(kind==='element-pinned')element.pinned=event.checked;if(kind==='element-entrance'){element.entrance=event.value;if(!element.anim||Array.isArray(element.anim))element.anim={};element.anim.type=event.value}if(kind==='element-sfx')element.sfx=event.value;if(kind==='element-font')element.font=event.value;if(kind==='element-color')element.color=event.value;if(kind==='element-weight')element.weight=Number(event.value)||400;if(kind==='element-per-word'){if(!element.anim||Array.isArray(element.anim))element.anim={};element.anim.perWord=event.checked}if(kind==='element-speed'){if(!element.anim||Array.isArray(element.anim))element.anim={};element.anim.speedMs=Math.max(1,Number(event.value)||1)}if(kind==='element-source-in')element.sourceInMs=Math.max(0,Number(event.value)||0);if(kind==='element-source-out')element.sourceOutMs=Math.max(0,Number(event.value)||0);if(kind==='element-muted')element.muted=event.checked;if(kind==='element-loop')element.loop=event.checked}changed();if(kind==='element-kind')draw()});\ndocument.getElementById('add').addEventListener('click',()=>{if(busy)return;ensure();const start=draft.strip.filter(board=>board.type==='canvas').reduce((total,board)=>total+Math.max(0,Number(board.durationMs)||0),0),id='portable-board-'+String(draft.strip.length+1)+'-'+String(revision+1);draft.strip=[...draft.strip,{panelId:id,type:'canvas',sceneId:null,background:'#ffffff',autoArrange:true,startMs:start,durationMs:5000,transitionIn:draft.strip.length?'swirl':'cut',elements:[]}];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.strip))throw Error('Whiteboard JSON must be a version 1 object with a strip.');draft=clone(value);changed();draw();status.textContent='Complete Whiteboard data applied to this workspace.'}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 project Whiteboard.'}});\nsave.addEventListener('click',async()=>{if(busy)return;const started=revision;busy=true;draw();status.textContent='Checking the complete Whiteboard…';try{const result=await StillMade.run({board:source,updatedBoard:draft,commit:true});draft=clone(result.board);if(started===revision)status.textContent='Whiteboard changes are ready below. Review the before and after, then choose Apply.'}catch(error){status.textContent=error.message||'The Whiteboard could not be prepared.'}finally{busy=false;draw()}});\nconst renderWorkspaceSync=(versions,presence)=>{const saved=Array.isArray(versions)?versions.slice(0,20):[],people=Array.isArray(presence)?presence.slice(0,20):[];StillMade.render('workspaceSync',[{tag:'p',text:saved.length?String(saved.length)+' saved revision'+(saved.length===1?'':'s')+' available.':'No saved revisions yet.'},{tag:'ul',children:saved.map((item,index)=>({tag:'li',text:text(item.title||item.label||item.id||'Revision '+String(index+1))+' · '+text(item.createdAt||item.updatedAt||item.date||'Saved')}))},{tag:'p',text:people.length?String(people.length)+' collaborator'+(people.length===1?'':'s')+' active or recently active.':'No collaborator presence available.'},{tag:'ul',children:people.map((item,index)=>({tag:'li',text:text(item.name||item.displayName||item.id||'Collaborator '+String(index+1))+(item.editing?' · editing '+text(item.editing):'' )}))}])};\nStillMade.onInput(values=>{source=clone(values.board||{schemaVersion:1,strip:[]});draft=clone(source);connectedImage=values.generatedImage?clone(values.generatedImage):null;document.getElementById('connectedMedia').textContent=connectedImage?'Ready: '+text(connectedImage.assetId)+' · '+text(connectedImage.versionId):'Connect Generate image or Import media to place an exact stored image.';revision+=1;draw();renderWorkspaceSync(values.versions,values.presence);status.textContent='Edit boards and elements. Existing text, media, motion, and custom fields are retained.'});\n\nconst mediaAction=async(action,extra={})=>{if(busy)return;busy=true;draw();try{await StillMade.run({board:source,updatedBoard:draft,commit:false,action,...extra});status.textContent='The approved action is ready below.'}catch(error){status.textContent=error.message||'The media action could not be prepared.'}finally{busy=false;draw()}};document.getElementById('generatePresenter').addEventListener('click',()=>mediaAction('presenter',{presenterPrompt:document.getElementById('presenterPrompt').value}));document.getElementById('qualityReview').addEventListener('click',()=>mediaAction('quality'));document.getElementById('renderFinal').addEventListener('click',()=>mediaAction('render'));\n\ndocument.getElementById('prepareEditor').addEventListener('click',async()=>{if(busy)return;busy=true;draw();status.textContent='Preparing the editable timeline…';try{const result=await StillMade.run({board:source,updatedBoard:draft,commit:true});status.textContent=String(result.editorTimeline.clips.length)+' Whiteboard clips are ready below for a connected Editor Step.'}catch(error){status.textContent=error.message||'The Editor handoff could not be prepared.'}finally{busy=false;draw()}});\n\nconst updatePlayback=()=>{const total=Math.max(1,...draft.strip.filter(board=>board.type==='canvas').map(board=>(Number(board.startMs)||0)+Math.max(1,Number(board.durationMs)||1)));playheadMs=Math.max(0,Math.min(total,playheadMs));const slider=document.getElementById('playhead');slider.max=String(total);slider.value=String(playheadMs);document.getElementById('playTime').textContent=(playheadMs/1000).toFixed(1)+'s';draft.strip.forEach((board,index)=>{const frame=document.getElementById('preview-frame-'+String(index));if(frame)frame.classList.toggle('active',playheadMs>=(Number(board.startMs)||0)&&playheadMs<(Number(board.startMs)||0)+Math.max(1,Number(board.durationMs)||1))})};document.getElementById('playhead').addEventListener('input',event=>{playheadMs=Number(event.target.value)||0;updatePlayback()});document.getElementById('advance').addEventListener('click',()=>{const total=Number(document.getElementById('playhead').max)||1;playheadMs=playheadMs+500>=total?0:playheadMs+500;updatePlayback()});\n\ndocument.getElementById('addConnectedImage').addEventListener('click',()=>{if(busy||!connectedImage)return;const selectedBoard=selectedElement?Number(selectedElement.split(':')[0]):NaN,board=Number.isFinite(selectedBoard)?draft.strip.find((item,index)=>index===selectedBoard&&item.type==='canvas'):draft.strip.find(item=>item.type==='canvas');if(!board){status.textContent='Add a board before placing connected media.';return}if(!Array.isArray(board.elements))board.elements=[];const image={...clone(connectedImage),asset:clone(connectedImage),id:'connected-image-'+String(board.elements.length+1)+'-'+String(revision+1),kind:'image',src:connectedImage.url,url:connectedImage.url,pos:{x:.5,y:.5},size:.28,appearMs:0,leaveMs:null,pinned:false};board.elements=[...board.elements,image];selectedElement=String(draft.strip.indexOf(board))+':'+String(board.elements.length-1);changed();draw();status.textContent='Connected image placed at the center. Adjust its position and size below.'});document.getElementById('previewSelected').addEventListener('click',async()=>{const parts=selectedElement.split(':'),board=draft.strip.find((item,index)=>index===Number(parts.find((item,index)=>index===0))),element=board&&board.elements&&board.elements.find((item,index)=>index===Number(parts.find((item,index)=>index===1))),reference=element&&element.asset&&element.asset.kind==='image'?element.asset:element;if(!reference||reference.kind!=='image'||!reference.url){status.textContent='Select an exact image with a preview URL first.';return}try{const preview=await StillMade.previewImage(reference),image=document.getElementById('mediaPreview');image.src=preview.url;image.hidden=false;status.textContent='Showing the trusted bounded preview for the selected image.'}catch(error){status.textContent=error.message||'This image preview is unavailable.'}});\n"},"tests":[{"name":"Retain the complete current Whiteboard","input":{"board":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"background":"#ffffff","elements":[{"id":"image-1","kind":"image","src":"https://media.example/retained.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}]}]},"updatedBoard":{"schemaVersion":1,"strip":[]},"commit":false},"expected":{"board":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"background":"#ffffff","elements":[{"id":"image-1","kind":"image","src":"https://media.example/retained.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}]}]},"editorTimeline":{"schemaVersion":1,"activeTimelineId":"whiteboard","clips":[{"id":"board-1","start":0,"duration":5,"canvas":{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"background":"#ffffff","elements":[{"id":"image-1","kind":"image","src":"https://media.example/retained.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}]}}],"audioItems":[],"captions":[],"textOverlays":[],"effects":[],"targetDuration":5},"edit":{"schemaVersion":1,"title":"Update Whiteboard (1 boards)","field":"board","before":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"background":"#ffffff","elements":[{"id":"image-1","kind":"image","src":"https://media.example/retained.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}]}]},"after":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"background":"#ffffff","elements":[{"id":"image-1","kind":"image","src":"https://media.example/retained.png","assetId":"asset-1","versionId":"version-2","custom":{"keep":true}}]}]}}}},{"name":"Prepare one exact complete Whiteboard update","input":{"board":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"transitionIn":"cut","elements":[{"id":"title-1","kind":"text","text":"Original","appearMs":0}]}]},"updatedBoard":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":6000,"transitionIn":"fade","elements":[{"id":"title-1","kind":"text","text":"Updated","appearMs":250}]}]},"commit":true},"expected":{"board":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":6000,"transitionIn":"fade","elements":[{"id":"title-1","kind":"text","text":"Updated","appearMs":250}]}]},"editorTimeline":{"schemaVersion":1,"activeTimelineId":"whiteboard","clips":[{"id":"board-1","start":0,"duration":6,"canvas":{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":6000,"transitionIn":"fade","elements":[{"id":"title-1","kind":"text","text":"Updated","appearMs":250}]}}],"audioItems":[],"captions":[],"textOverlays":[],"effects":[],"targetDuration":6},"edit":{"schemaVersion":1,"title":"Update Whiteboard (1 boards)","field":"board","before":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":5000,"transitionIn":"cut","elements":[{"id":"title-1","kind":"text","text":"Original","appearMs":0}]}]},"after":{"schemaVersion":1,"meta":{"aspect":"16:9"},"strip":[{"panelId":"board-1","type":"canvas","startMs":0,"durationMs":6000,"transitionIn":"fade","elements":[{"id":"title-1","kind":"text","text":"Updated","appearMs":250}]}]}}}}]}