stillmade-sdk 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MODULES.md +29 -11
- package/PASTE_TO_LLM.md +7 -6
- package/SDK.md +16 -15
- package/UI_AESTHETIC.md +6 -0
- package/blocks/narration/package.js +1 -1
- package/blocks/narration/versions/1.0.1/package.js +221 -0
- package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +769 -0
- package/blocks/native/sm.board-editor/versions/2.2.11/package.js +551 -0
- package/blocks/native/sm.panel-strip/versions/2.4.2/package.js +1316 -0
- package/blocks/native/sm.pipeline/versions/2.2.6/package.js +1095 -0
- package/blocks/screen-recorder/package.js +1 -1
- package/blocks/screen-recorder/versions/1.0.1/package.js +223 -0
- package/package.json +1 -1
- package/packages/block-cli/cli.js +28 -17
- package/packages/block-cli/dev-host/assets/host.js +16 -16
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/dev-server.js +14 -2
- package/packages/block-cli/ready-gate.js +3 -2
- package/packages/block-platform/catalog.js +1 -1
- package/packages/block-platform/motion-starters.js +14 -0
- package/packages/block-platform/project-type-starters.js +20 -7
- package/packages/block-sdk/capabilities.js +1 -1
- package/packages/block-sdk/example-media-data.js +2 -1
- package/packages/block-sdk/example-media.js +1 -0
- package/packages/block-sdk/first-party-identities.js +6 -0
- package/packages/block-sdk/first-party-packages.js +7 -1
- package/packages/block-sdk/media-transform.js +17 -4
- package/packages/block-sdk/mock-host.js +24 -5
- package/packages/block-sdk/module-hosted.js +12 -4
- package/packages/block-sdk/semantic-relationships.js +6 -0
- package/packages/block-sdk/testing.js +5 -3
- package/packages/block-sdk/view-design.js +56 -1
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// Generated by scripts/build-owned-blocks.mjs. Edit the owned source files.
|
|
2
|
+
export default {
|
|
3
|
+
"manifest": {
|
|
4
|
+
"schemaVersion": 1,
|
|
5
|
+
"sdkVersion": "0.1.0",
|
|
6
|
+
"id": "sm.board-editor",
|
|
7
|
+
"version": "2.2.11",
|
|
8
|
+
"name": "Whiteboard",
|
|
9
|
+
"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 with exact project media-library browsing and trusted previews.",
|
|
10
|
+
"kind": "workspace",
|
|
11
|
+
"runtime": "javascript",
|
|
12
|
+
"entry": "src/run.js",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"provenance": {
|
|
15
|
+
"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",
|
|
16
|
+
"portableSources": [
|
|
17
|
+
{
|
|
18
|
+
"kind": "original",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"paths": [
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"src/run.js",
|
|
23
|
+
"src/view.json"
|
|
24
|
+
],
|
|
25
|
+
"evidence": [
|
|
26
|
+
"upstream/0/LICENSE",
|
|
27
|
+
"upstream/0/src/run.js",
|
|
28
|
+
"upstream/0/src/view.json"
|
|
29
|
+
],
|
|
30
|
+
"changes": "Original implementation declared by the packager; no reused repository or dependency code."
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"inputs": {
|
|
35
|
+
"board": {
|
|
36
|
+
"type": "board-document",
|
|
37
|
+
"context": "board",
|
|
38
|
+
"primary": true,
|
|
39
|
+
"required": false,
|
|
40
|
+
"default": {
|
|
41
|
+
"schemaVersion": 1,
|
|
42
|
+
"strip": []
|
|
43
|
+
},
|
|
44
|
+
"description": "The complete current Whiteboard document. Existing text, media references, motion, timing, and custom fields are retained.",
|
|
45
|
+
"semantic": "whiteboard_document"
|
|
46
|
+
},
|
|
47
|
+
"updatedBoard": {
|
|
48
|
+
"type": "board-document",
|
|
49
|
+
"required": false,
|
|
50
|
+
"default": {
|
|
51
|
+
"schemaVersion": 1,
|
|
52
|
+
"strip": []
|
|
53
|
+
},
|
|
54
|
+
"description": "The complete edited Whiteboard document prepared in this workspace.",
|
|
55
|
+
"semantic": "whiteboard_document"
|
|
56
|
+
},
|
|
57
|
+
"commit": {
|
|
58
|
+
"type": "boolean",
|
|
59
|
+
"default": false,
|
|
60
|
+
"description": "Prepare the edited Whiteboard for StillMade review and Apply.",
|
|
61
|
+
"semantic": "apply_changes",
|
|
62
|
+
"required": false
|
|
63
|
+
},
|
|
64
|
+
"action": {
|
|
65
|
+
"type": "text",
|
|
66
|
+
"required": false,
|
|
67
|
+
"default": "none",
|
|
68
|
+
"description": "The Block-owned interface selects a declared host action.",
|
|
69
|
+
"semantic": "whiteboard_action"
|
|
70
|
+
},
|
|
71
|
+
"presenterPrompt": {
|
|
72
|
+
"type": "text",
|
|
73
|
+
"required": false,
|
|
74
|
+
"default": "Friendly presenter explaining the narration",
|
|
75
|
+
"description": "Presenter direction sent only after approval.",
|
|
76
|
+
"semantic": "presenter_prompt"
|
|
77
|
+
},
|
|
78
|
+
"generatedImage": {
|
|
79
|
+
"type": "image",
|
|
80
|
+
"imageMode": "reference",
|
|
81
|
+
"required": false,
|
|
82
|
+
"description": "An exact stored image from an approved generation or import Block, available for placement on the selected board.",
|
|
83
|
+
"semantic": "image"
|
|
84
|
+
},
|
|
85
|
+
"versions": {
|
|
86
|
+
"type": "metadata[]",
|
|
87
|
+
"context": "versions",
|
|
88
|
+
"required": false,
|
|
89
|
+
"description": "Host-backed saved revision records for this exact project workspace.",
|
|
90
|
+
"semantic": "revision_history"
|
|
91
|
+
},
|
|
92
|
+
"presence": {
|
|
93
|
+
"type": "metadata[]",
|
|
94
|
+
"required": false,
|
|
95
|
+
"description": "Bounded collaborator presence supplied through a declared connection or host project context.",
|
|
96
|
+
"semantic": "collaborator_presence"
|
|
97
|
+
},
|
|
98
|
+
"assets": {
|
|
99
|
+
"type": "asset[]",
|
|
100
|
+
"context": "assets",
|
|
101
|
+
"required": false,
|
|
102
|
+
"description": "Exact stored project media versions available for bounded browsing and local attachment.",
|
|
103
|
+
"semantic": "project_media"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"outputs": {
|
|
107
|
+
"board": {
|
|
108
|
+
"type": "board-document",
|
|
109
|
+
"primary": true,
|
|
110
|
+
"role": "whiteboard_document",
|
|
111
|
+
"description": "The complete edited Whiteboard document for the next Step.",
|
|
112
|
+
"semantic": "whiteboard_document"
|
|
113
|
+
},
|
|
114
|
+
"edit": {
|
|
115
|
+
"type": "workspace-edit",
|
|
116
|
+
"description": "An exact before/after Whiteboard change reviewed before it updates the project.",
|
|
117
|
+
"semantic": "workspace_edit"
|
|
118
|
+
},
|
|
119
|
+
"presenterAction": {
|
|
120
|
+
"type": "approval",
|
|
121
|
+
"required": false,
|
|
122
|
+
"role": "portable_media_action",
|
|
123
|
+
"description": "Generate presenter poses and return a reviewable Whiteboard edit.",
|
|
124
|
+
"semantic": "portable_media_action"
|
|
125
|
+
},
|
|
126
|
+
"qualityAction": {
|
|
127
|
+
"type": "approval",
|
|
128
|
+
"required": false,
|
|
129
|
+
"role": "portable_media_action",
|
|
130
|
+
"description": "Analyze exact saved images.",
|
|
131
|
+
"semantic": "portable_media_action"
|
|
132
|
+
},
|
|
133
|
+
"renderAction": {
|
|
134
|
+
"type": "approval",
|
|
135
|
+
"required": false,
|
|
136
|
+
"role": "portable_media_action",
|
|
137
|
+
"description": "Render the current board sequence.",
|
|
138
|
+
"semantic": "portable_media_action"
|
|
139
|
+
},
|
|
140
|
+
"actionEdit": {
|
|
141
|
+
"type": "workspace-edit",
|
|
142
|
+
"required": false,
|
|
143
|
+
"description": "Reviewable presenter placement result.",
|
|
144
|
+
"semantic": "workspace_edit"
|
|
145
|
+
},
|
|
146
|
+
"qualityReport": {
|
|
147
|
+
"type": "metadata",
|
|
148
|
+
"required": false,
|
|
149
|
+
"semantic": "quality_report",
|
|
150
|
+
"description": "Structured quality findings for the reviewed media."
|
|
151
|
+
},
|
|
152
|
+
"editorTimeline": {
|
|
153
|
+
"type": "timeline",
|
|
154
|
+
"role": "editor_handoff",
|
|
155
|
+
"description": "An editable timeline preserving every canvas board for a downstream Editor Step.",
|
|
156
|
+
"semantic": "editor_handoff"
|
|
157
|
+
},
|
|
158
|
+
"revision": {
|
|
159
|
+
"type": "metadata",
|
|
160
|
+
"description": "A deterministic revision event for durable host storage and collaboration propagation.",
|
|
161
|
+
"required": false,
|
|
162
|
+
"semantic": "revision_event"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"permissions": {
|
|
166
|
+
"project": [
|
|
167
|
+
"context.board.read",
|
|
168
|
+
"workspace.board.propose",
|
|
169
|
+
"context.versions.read",
|
|
170
|
+
"context.assets.read"
|
|
171
|
+
],
|
|
172
|
+
"network": [],
|
|
173
|
+
"filesystem": [],
|
|
174
|
+
"secrets": [],
|
|
175
|
+
"actions": [
|
|
176
|
+
"presenter.generate",
|
|
177
|
+
"quality.analyze",
|
|
178
|
+
"final.render"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
"ui": [],
|
|
182
|
+
"platforms": {
|
|
183
|
+
"phone": {
|
|
184
|
+
"supported": true
|
|
185
|
+
},
|
|
186
|
+
"browser": {
|
|
187
|
+
"supported": true
|
|
188
|
+
},
|
|
189
|
+
"desktop": {
|
|
190
|
+
"supported": true
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"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.11'}},resultPort:null};\nreturn outputs;\n",
|
|
195
|
+
"view": {
|
|
196
|
+
"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=\"undo\" data-stillmade-action=\"undo\" type=\"button\" class=\"secondary\">Undo</button><button id=\"redo\" data-stillmade-action=\"redo\" type=\"button\" class=\"secondary\">Redo</button><button id=\"reset\" data-stillmade-action=\"reset-changes\" type=\"button\" class=\"secondary\">Reset shared changes</button><button id=\"add\" data-stillmade-action=\"add-board\" 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 <output id=\"syncStatus\" role=\"status\" aria-live=\"polite\"></output>\n <section id=\"recovery\" hidden><p>Your local draft is retained. Retry it, or discard only your unconfirmed edits.</p><div class=\"actions\"><button id=\"retry\" data-stillmade-action=\"retry-sharing\" type=\"button\" class=\"secondary\">Retry shared edits</button><button id=\"discard\" data-stillmade-action=\"discard-unsent-edits\" type=\"button\" class=\"secondary\">Discard unconfirmed edits</button></div><div id=\"sourceRecovery\" hidden><p>The project source changed. Resetting adopts that source and clears this shared Whiteboard draft for everyone.</p><label><input id=\"confirmSourceReset\" type=\"checkbox\">I understand this resets the shared draft for everyone.</label><button id=\"resetSource\" data-stillmade-action=\"use-project-source\" type=\"button\" class=\"secondary\">Use current project source</button></div></section>\n <p id=\"sharedResults\" aria-live=\"polite\">No shared Whiteboard result yet.</p>\n <section><h2>Saved revisions and collaborators</h2><div id=\"workspaceSync\"></div></section>\n <section><h2>Project media library</h2><p>Browse exact stored image versions supplied by the host.</p><div id=\"mediaLibrary\"></div><img id=\"libraryPreview\" alt=\"Trusted project media preview\" hidden></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\" data-stillmade-action=\"add-connected-image\" type=\"button\" class=\"secondary\">Add connected image</button><button id=\"previewSelected\" data-stillmade-action=\"preview-image\" 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\" data-stillmade-action=\"prepare-editor\" type=\"button\" class=\"secondary\">Prepare Editor handoff</button></div><div class=\"playback\"><button id=\"advance\" data-stillmade-action=\"advance-preview\" 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 id=\"rawDetails\"><summary>Complete Whiteboard data</summary><p>Every field and placed media reference is retained. Unfinished JSON is shared separately; apply it before reviewing. Applying refuses a stale base rather than overwriting a teammate.</p><label for=\"raw\">Complete Whiteboard JSON</label><textarea id=\"raw\" class=\"raw\" spellcheck=\"false\"></textarea><div class=\"actions\"><button id=\"applyRaw\" data-stillmade-action=\"apply-json\" type=\"button\" class=\"secondary\">Apply JSON to workspace</button><button id=\"clearRaw\" data-stillmade-action=\"discard-json\" type=\"button\" class=\"secondary\">Discard shared JSON scratch</button></div></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\" data-stillmade-action=\"generate-presenter\" type=\"button\" class=\"secondary\">Generate presenter poses</button><button id=\"qualityReview\" data-stillmade-action=\"review-quality\" type=\"button\" class=\"secondary\">Review image quality</button><button id=\"renderFinal\" data-stillmade-action=\"render-video\" type=\"button\">Render final video</button></div></section></main>\n",
|
|
197
|
+
"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",
|
|
198
|
+
"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,mediaAssets=[],boardPage=0;\nlet shared=null,scratchShared=null,optionsShared=null,connectionError='',permission=StillMade.getShared().canEdit===true;\nlet scratch={text:null,base:null},options={presenterPrompt:'Friendly presenter explaining the narration'},past=null,future=null,lastContent=null,sourceStamp='',rawDocument=null,rawWasScratch=false;\nconst elementPages=new Map(),identityTokens=new Map(),tokenCache=new Map();\nconst clone=value=>JSON.parse(JSON.stringify(value));\nconst text=value=>value===undefined||value===null?'':String(value);\nconst same=(a,b)=>a===b||JSON.stringify(a)===JSON.stringify(b);\nconst nextId=prefix=>prefix+'-'+String(Date.now())+'-'+Math.random().toString(36).slice(2);\nconst token=value=>{const valueText=JSON.stringify(value);if(tokenCache.has(valueText))return tokenCache.get(valueText);const key=Array.from(valueText).reduce((hash,char)=>(Math.imul(hash,31)+char.charCodeAt(0))>>>0,2166136261).toString(36)+'-'+Array.from(valueText).reduce((hash,char)=>(Math.imul(hash,131)+char.charCodeAt(0))>>>0,5381).toString(36);if(identityTokens.has(key)&&identityTokens.get(key)!==valueText)throw Error('Two control identities collide; use the complete JSON editor.');identityTokens.set(key,valueText);tokenCache.set(valueText,key);return key;};\nconst boardKey=index=>{const board=draft.strip.at(index);return token(board&&board.panelId?['board',board.panelId,draft.strip.slice(0,index).filter(item=>item.panelId===board.panelId).length]:['legacy-board',index]);};\nconst elementKey=(boardIndex,index)=>{const board=draft.strip.at(boardIndex),element=board&&Array.isArray(board.elements)?board.elements.at(index):null;return token(element&&element.id?['element',element.id,board.elements.slice(0,index).filter(item=>item.id===element.id).length]:['legacy-element',index]);};\nconst controllers=()=>[shared,scratchShared,optionsShared].filter(Boolean);\nconst canChange=()=>permission&&!busy&&!connectionError&&!!shared&&!controllers().some(item=>item.getStatus().error);\nconst canRun=()=>canChange()&&scratch.text===null;\nconst setMessage=value=>{status.textContent=value;};\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},type==='textarea'?{tag:'textarea',id:'field-'+action,action,value:text(value),disabled:!canChange(),label}:{tag:'input',id:'field-'+action,type,action,value:Number.isFinite(value)?value:text(value),disabled:!canChange(),label}]});\nconst choice=(label,action,value,options)=>({tag:'label',children:[{tag:'span',text:label},{tag:'select',id:'field-'+action,action,value:text(value),disabled:!canChange(),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',id:'field-'+action,type:'checkbox',action,checked:!!value,disabled:!canChange(),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:!canChange()||disabled});\nconst elementCard=(element,boardIndex,elementIndex)=>{const key=boardKey(boardIndex)+':'+elementKey(boardIndex,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',id:'element-'+key,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:!canChange()},{tag:'button',action:'remove-element:'+key,text:'Remove',className:'danger',disabled:!canChange()}]}]},{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),key=boardKey(index),elements=Array.isArray(board.elements)?board.elements:[],page=Math.min(elementPages.get(key)||0,Math.max(0,Math.ceil(elements.length/4)-1));elementPages.set(key,page);return {tag:'article',id:'board-'+key,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:'+key,'Up',position===0),moveButton('move-down:'+key,'Down',position===positions.length-1),{tag:'button',action:'add-text:'+key,text:'Add text',className:'secondary',disabled:!canChange()},{tag:'button',action:'remove-board:'+key,text:'Remove board',className:'danger',disabled:!canChange()||positions.length<=1}]}]},{tag:'div',className:'grid',children:[field('Board ID','board-id:'+key,board.panelId),field('Scene ID','board-scene:'+key,board.sceneId),field('Background','board-background:'+key,board.background),field('Starts (ms)','board-start:'+key,board.startMs,'number'),field('Length (ms)','board-duration:'+key,board.durationMs,'number'),choice('Transition','board-transition:'+key,board.transitionIn||'cut',optionList(board.transitionIn,['cut','fade','swirl','wipe','slide'])),field('Transition length (ms)','board-transition-ms:'+key,board.transitionMs,'number'),toggle('Automatic arrangement','board-arrange:'+key,board.autoArrange!==false)]},{tag:'div',className:'elements',children:Array.isArray(board.elements)&&board.elements.length?[{tag:'div',className:'row',children:[{tag:'button',action:'elements-back:'+key,text:'Previous elements',disabled:page===0},{tag:'span',text:'Elements '+String(page*4+1)+'–'+String(Math.min(elements.length,page*4+4))+' of '+String(elements.length)},{tag:'button',action:'elements-next:'+key,text:'Next elements',disabled:(page+1)*4>=elements.length}]},...elements.slice(page*4,page*4+4).map((element,elementIndex)=>elementCard(element,index,page*4+elementIndex))]:[{tag:'p',text:'No elements yet. Add text, request a presenter, or use Complete Whiteboard data for retained media.'}]}]}};\nconst validBoard=value=>{if(!value||Array.isArray(value)||value.schemaVersion!==1||!Array.isArray(value.strip)||!value.strip.every(item=>item&&typeof item==='object'&&!Array.isArray(item)))throw Error('Whiteboard JSON must be a version 1 object with a strip of board rows.');return clone(value);};\nconst renumber=()=>{let start=0;draft={...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 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-'+boardKey(index));if(frame)frame.classList.toggle('active',playheadMs>=(Number(board.startMs)||0)&&playheadMs<(Number(board.startMs)||0)+Math.max(1,Number(board.durationMs)||1));});};\nconst syncControls=()=>{\n const states=controllers().map(item=>item.getStatus()),error=connectionError||states.find(item=>item.error)?.error||'',sourceConflict=states.some(item=>item.sourceConflict),pending=states.some(item=>item.dirty||item.pending);\n document.getElementById('syncStatus').textContent=error?'Shared edits paused. Your local draft is retained. '+error:!permission?'View only. Shared changes and results remain live.':pending?'Sharing changes…':'Shared draft is up to date locally; the project indicator reports saving.';\n document.getElementById('recovery').hidden=!error;document.getElementById('sourceRecovery').hidden=!sourceConflict;\n document.getElementById('retry').disabled=!permission||busy||sourceConflict||!error;\n document.getElementById('discard').disabled=!permission||busy||sourceConflict||!error;\n document.getElementById('resetSource').disabled=!permission||busy||!sourceConflict||!document.getElementById('confirmSourceReset').checked;\n ['reset','add','applyRaw','clearRaw'].forEach(id=>{document.getElementById(id).disabled=!canChange();});\n ['save','prepareEditor','generatePresenter','qualityReview','renderFinal'].forEach(id=>{document.getElementById(id).disabled=!canRun();});\n document.getElementById('undo').disabled=!canChange()||!past;document.getElementById('redo').disabled=!canChange()||!future;\n document.getElementById('addConnectedImage').disabled=!canChange()||!connectedImage;\n raw.disabled=!canChange();aspect.disabled=!canChange();document.getElementById('presenterPrompt').disabled=!canChange();\n};\nconst formatRaw=()=>{if(!document.getElementById('rawDetails').hasAttribute('open')||scratch.text!==null)return;if(rawDocument!==draft||rawWasScratch){const value=JSON.stringify(draft,null,2);if(raw.value!==value)raw.value=value;rawDocument=draft;rawWasScratch=false;}};\nconst renderRaw=()=>{if(scratch.text!==null){if(raw.value!==scratch.text)raw.value=scratch.text;rawWasScratch=true;return;}if(document.getElementById('rawDetails').hasAttribute('open')&&(rawDocument!==draft||rawWasScratch))StillMade.scheduleSharedEdit('boardRawPreview',formatRaw);};\nconst draw=()=>{\n const rows=draft.strip,positions=canvasPositions();boardPage=Math.min(boardPage,Math.max(0,Math.ceil(rows.length/2)-1));\n StillMade.render('sequencePreview',rows.map((board,boardIndex)=>({board,boardIndex})).filter(item=>item.board.type==='canvas').slice(0,30).map(({board,boardIndex},index)=>({tag:'article',id:'preview-frame-'+boardKey(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 key=boardKey(boardIndex)+':'+elementKey(boardIndex,elementIndex),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-'+key,className:'spatial-item x-'+String(x)+' y-'+String(y)+sizeClass+(selectedElement===key?' selected':''),action:'select-element:'+key,text:text(element.text||element.descriptor||element.id||element.kind||'Element').slice(0,160)};})}]})));\n updatePlayback();\n StillMade.render('boards',rows.length?[{tag:'div',className:'row',children:[{tag:'button',action:'boards-back',text:'Previous boards',disabled:boardPage===0},{tag:'span',text:'Rows '+String(boardPage*2+1)+'–'+String(Math.min(rows.length,boardPage*2+2))+' of '+String(rows.length)},{tag:'button',action:'boards-next',text:'Next boards',disabled:(boardPage+1)*2>=rows.length}]},...rows.slice(boardPage*2,boardPage*2+2).map((board,index)=>boardCard(board,boardPage*2+index))]:[{tag:'p',text:'No boards yet. Add one to begin.'}]);\n renderRaw();const aspectValue=text(draft.meta&&draft.meta.aspect);if(aspect.value!==aspectValue)aspect.value=aspectValue;\n summary.textContent=String(positions.length)+' board'+(positions.length===1?'':'s')+' in this complete Whiteboard';syncControls();\n};\n// Mutation entry points copy only the touched object path. Immutable references\n// keep exact undo bases without traversing the complete Whiteboard while typing.\nconst contentChanged=()=>{revision+=1;if(lastContent&&lastContent!==draft){past={before:lastContent,after:draft};future=null;}lastContent=draft;shared.changed();renderRaw();setMessage('Changes are shared with the project. Review them to prepare a project update.');syncControls();};\nconst flushDocuments=async()=>{await Promise.all(controllers().map(item=>item.flush()));};\nconst renderMediaLibrary=()=>StillMade.render('mediaLibrary',mediaAssets.length?mediaAssets.slice(0,50).map((asset,index)=>({tag:'article',className:'card',children:[{tag:'strong',text:text(asset.title||asset.name||asset.assetId)+' · '+text(asset.versionId)},{tag:'div',className:'row',children:[{tag:'button',action:'library-preview:'+String(index),text:'Preview',className:'secondary'},{tag:'button',action:'library-attach:'+String(index),text:'Place on board',className:'secondary',disabled:!canChange()}]}]})):[{tag:'p',text:'No stored project images are available.'}]);\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):'')}))}]);};\nconst displayResults=outputs=>{const clips=outputs&&outputs.editorTimeline&&outputs.editorTimeline.clips,board=outputs&&outputs.board,actions=['presenterAction','qualityAction','renderAction'].filter(key=>Object.keys(outputs||{}).includes(key));document.getElementById('sharedResults').textContent=board?'Shared Whiteboard proposal · '+String(Array.isArray(board.strip)?board.strip.length:0)+' rows'+(Array.isArray(clips)?' · '+String(clips.length)+' Editor clips':'')+(actions.length?' · An action is ready for review below.':'')+' Your editable draft is not replaced by results.':'No shared Whiteboard result yet.';};\nconst runDraft=async(mode,action)=>{\n if(!canRun())return;busy=true;draw();renderMediaLibrary();setMessage('Sharing the latest draft before preparing the result…');\n try{await flushDocuments();if(!permission||connectionError||controllers().some(item=>item.getStatus().error)||scratch.text!==null)throw Error('Editing access or the draft changed. Review the shared status before running.');\n const currentRevision=revision,result=await StillMade.run({board:clone(source),updatedBoard:clone(draft),commit:true,...(action?{action}:{}),presenterPrompt:options.presenterPrompt});\n // Results are proposals, never a replacement for a newer local/remote draft.\n displayResults(StillMade.getShared().outputs||result);\n if(currentRevision===revision)setMessage(mode==='editor'?String(result.editorTimeline.clips.length)+' Whiteboard clips are ready below for a connected Editor Step.':action?'The action is ready below for review.':'Whiteboard changes are ready below. Review the before and after, then choose Apply.');\n }catch(error){setMessage(error.message||'The Whiteboard result could not be prepared.');}finally{busy=false;draw();renderMediaLibrary();}\n};\naspect.addEventListener('input',()=>{if(!canChange()){draw();return;}draft={...draft,meta:{...(!Array.isArray(draft.meta)&&draft.meta||{}),aspect:aspect.value}};contentChanged();});\ndocument.getElementById('rawDetails').addEventListener('toggle',()=>{if(scratch.text===null)formatRaw();else renderRaw();});\nraw.addEventListener('input',()=>{if(!canChange()){renderRaw();return;}scratch={text:raw.value,base:scratch.text===null?rawDocument||draft:scratch.base};rawWasScratch=true;scratchShared.changed();syncControls();});\ndocument.getElementById('presenterPrompt').addEventListener('input',()=>{if(!canChange()){document.getElementById('presenterPrompt').value=options.presenterPrompt;return;}options.presenterPrompt=document.getElementById('presenterPrompt').value;optionsShared.changed();});\ndocument.getElementById('applyRaw').addEventListener('click',async()=>{\n if(!canChange()||scratch.text===null)return;busy=true;draw();\n try{await scratchShared.flush();if(!permission||controllers().some(item=>item.getStatus().error))throw Error('Editing is unavailable; your JSON scratch is retained.');const pending=clone(scratch);if(!same(pending.base,draft))throw Error('The Whiteboard changed after this JSON draft began. Your scratch is retained; copy it, then discard the shared scratch to start from the latest board.');\n const value=validBoard(JSON.parse(pending.text));draft=value;contentChanged();draw();await shared.flush();\n if(!permission)throw Error('Editing access changed; the shared JSON scratch is retained.');\n if(same(scratch,pending)){scratch={text:null,base:null};scratchShared.changed();await scratchShared.flush();}\n setMessage('Complete Whiteboard data applied to the shared workspace.');\n }catch(error){setMessage(error.message);}finally{busy=false;draw();}\n});\ndocument.getElementById('clearRaw').addEventListener('click',()=>{if(!canChange())return;scratch={text:null,base:null};scratchShared.changed();renderRaw();syncControls();});\ndocument.getElementById('reset').addEventListener('click',()=>{if(!canChange())return;draft=source;contentChanged();draw();setMessage('Shared changes reset to the current project Whiteboard.');});\ndocument.getElementById('undo').addEventListener('click',()=>{if(!canChange()||!past)return;const entry=past;if(draft!==entry.after){past=null;draw();return;}draft=entry.before;revision+=1;lastContent=draft;past=null;future=entry;shared.changed();draw();});\ndocument.getElementById('redo').addEventListener('click',()=>{if(!canChange()||!future)return;const entry=future;if(draft!==entry.before){future=null;draw();return;}draft=entry.after;revision+=1;lastContent=draft;future=null;past=entry;shared.changed();draw();});\ndocument.getElementById('add').addEventListener('click',()=>{if(!canChange())return;const start=draft.strip.filter(board=>board.type==='canvas').reduce((total,board)=>total+Math.max(0,Number(board.durationMs)||0),0);draft={...draft,strip:[...draft.strip,{panelId:nextId('portable-board'),type:'canvas',sceneId:null,background:'#ffffff',autoArrange:true,startMs:start,durationMs:5000,transitionIn:draft.strip.length?'swirl':'cut',elements:[]}]};boardPage=Math.floor((draft.strip.length-1)/2);contentChanged();draw();});\nsave.addEventListener('click',()=>runDraft('review'));\ndocument.getElementById('prepareEditor').addEventListener('click',()=>runDraft('editor'));\ndocument.getElementById('generatePresenter').addEventListener('click',()=>runDraft('action','presenter'));\ndocument.getElementById('qualityReview').addEventListener('click',()=>runDraft('action','quality'));\ndocument.getElementById('renderFinal').addEventListener('click',()=>runDraft('action','render'));\ndocument.getElementById('playhead').addEventListener('input',event=>{playheadMs=Number(event.target.value)||0;updatePlayback();});\ndocument.getElementById('advance').addEventListener('click',()=>{const total=Number(document.getElementById('playhead').max)||1;playheadMs=playheadMs+500>=total?0:playheadMs+500;updatePlayback();});\nconst recover=async(kind)=>{\n if(!permission||busy)return;busy=true;draw();\n try{if(kind==='source'){if(!document.getElementById('confirmSourceReset').checked)return;await shared.resetSharedSource({discardSharedEdits:true});document.getElementById('confirmSourceReset').checked=false;past=null;future=null;lastContent=draft;}\n else await Promise.all(controllers().filter(item=>item.getStatus().error).map(item=>kind==='retry'?item.retry():item.discard()));\n setMessage('Shared draft recovery completed.');\n }catch(error){setMessage(error.message);}finally{busy=false;draw();renderMediaLibrary();}\n};\ndocument.getElementById('retry').addEventListener('click',()=>recover('retry'));\ndocument.getElementById('discard').addEventListener('click',()=>recover('discard'));\ndocument.getElementById('confirmSourceReset').addEventListener('change',syncControls);\ndocument.getElementById('resetSource').addEventListener('click',()=>recover('source'));\nStillMade.onAction(event=>{const parts=event.action.split(':'),kind=parts[0];if(kind==='boards-back'||kind==='boards-next'){boardPage=Math.max(0,boardPage+(kind==='boards-back'?-1:1));draw();return;}const boardIndex=draft.strip.findIndex((item,index)=>boardKey(index)===parts[1]);let board=boardIndex>=0?draft.strip.at(boardIndex):null;const elementIndex=board&&Array.isArray(board.elements)?board.elements.findIndex((item,index)=>elementKey(boardIndex,index)===parts[2]):-1;let element=elementIndex>=0?board.elements.at(elementIndex):null;if(kind==='elements-back'||kind==='elements-next'){elementPages.set(parts[1],Math.max(0,(elementPages.get(parts[1])||0)+(kind==='elements-back'?-1:1)));draw();return;}\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=boardKey(boardIndex)+':'+elementKey(boardIndex,elementIndex);draw();status.textContent='Selected '+text(element.text||element.descriptor||element.id||element.kind||'element')+' in the spatial preview.';return}if(!canChange())return;if(kind==='remove-board'&&board&&board.type==='canvas'&&positions.length>1){draft={...draft,strip:draft.strip.filter((item,position)=>position!==boardIndex)};renumber();contentChanged();draw();return}if(moveTarget!==undefined&&board){const target=draft.strip.find((item,position)=>position===moveTarget);draft={...draft,strip:draft.strip.map((item,position)=>position===moveTarget?board:position===boardIndex?target:item)};renumber();contentChanged();draw();return}if(!board||board.type!=='canvas')return;\n // Preserve exact undo/raw-scratch baselines: all subsequent field mutations\n // target detached shallow paths, never a previously shared document object.\n board={...board,...(Array.isArray(board.elements)?{elements:[...board.elements]}:{})};if(element){element={...element,...(element.pos&&typeof element.pos==='object'&&!Array.isArray(element.pos)?{pos:{...element.pos}}:{}),...(element.anim&&typeof element.anim==='object'&&!Array.isArray(element.anim)?{anim:{...element.anim}}:{})};board.elements=board.elements.map((item,index)=>index===elementIndex?element:item);}draft={...draft,strip:draft.strip.map((item,index)=>index===boardIndex?board:item)};\n if(kind==='add-text'){if(!Array.isArray(board.elements))board.elements=[];board.elements=[...board.elements,{id:nextId('text'),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}];contentChanged();draw();return}if(kind==='remove-element'&&element){board.elements=board.elements.filter((item,position)=>position!==elementIndex);contentChanged();draw();return}if(kind==='duplicate-element'&&element){const copy=clone(element);copy.id=nextId(text(element.kind)||'element');board.elements=[...board.elements.slice(0,elementIndex+1),copy,...board.elements.slice(elementIndex+1)];contentChanged();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);contentChanged();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}contentChanged();if(kind==='element-kind'||kind==='element-id'||kind==='board-id')draw()});\n\nconst selected=()=>{const parts=selectedElement.split(':'),boardIndex=draft.strip.findIndex((item,index)=>boardKey(index)===parts[0]),board=boardIndex>=0?draft.strip.at(boardIndex):null,elementIndex=board&&Array.isArray(board.elements)?board.elements.findIndex((item,index)=>elementKey(boardIndex,index)===parts[1]):-1;return {boardIndex,board,element:elementIndex>=0?board.elements.at(elementIndex):null};};\nconst attachImage=asset=>{if(!canChange())return;const picked=selected(),board=picked.board&&picked.board.type==='canvas'?picked.board:draft.strip.find(item=>item.type==='canvas');if(!board){setMessage('Add a board before placing media.');return;}const image={...clone(asset),asset:clone(asset),id:nextId('image'),kind:'image',src:asset.url,url:asset.url,pos:{x:.5,y:.5},size:.28,appearMs:0,leaveMs:null,pinned:false};const boardIndex=draft.strip.indexOf(board),elements=[...(Array.isArray(board.elements)?board.elements:[]),image];draft={...draft,strip:draft.strip.map((item,index)=>index===boardIndex?{...board,elements}:item)};selectedElement=boardKey(boardIndex)+':'+elementKey(boardIndex,elements.length-1);contentChanged();draw();setMessage('Exact stored image placed on the board.');};\ndocument.getElementById('addConnectedImage').addEventListener('click',()=>{if(connectedImage)attachImage(connectedImage);});\ndocument.getElementById('previewSelected').addEventListener('click',async()=>{const element=selected().element,reference=element&&element.asset&&element.asset.kind==='image'?element.asset:element;if(!reference||reference.kind!=='image'||!reference.url){setMessage('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;setMessage('Showing the trusted bounded preview for the selected image.');}catch(error){setMessage(error.message||'This image preview is unavailable.');}});\nStillMade.onAction(async event=>{if(!event.action.startsWith('library-'))return;const parts=event.action.split(':'),asset=mediaAssets.find((item,index)=>index===Number(parts[1]));if(!asset)return;if(parts[0]==='library-preview'){try{const preview=await StillMade.previewImage(asset),image=document.getElementById('libraryPreview');image.src=preview.url;image.hidden=false;setMessage('Showing a trusted preview of the exact stored version.');}catch(error){setMessage(error.message||'Preview unavailable.');}return;}if(parts[0]==='library-attach')attachImage(asset);});\nStillMade.onShared(snapshot=>{permission=snapshot.canEdit===true;displayResults(snapshot.outputs||{});draw();renderMediaLibrary();});\nStillMade.onInput(values=>{\n try{\n const incoming=validBoard(values.board||{schemaVersion:1,strip:[]}),stamp=JSON.stringify(incoming);\n connectedImage=values.generatedImage?clone(values.generatedImage):null;\n mediaAssets=(Array.isArray(values.assets)?values.assets:[]).filter(asset=>asset&&asset.kind==='image'&&asset.url).slice(0,50);\n document.getElementById('connectedMedia').textContent=connectedImage?'Ready: '+text(connectedImage.assetId)+' · '+text(connectedImage.versionId):'Connect Generate image or Import media to place an exact stored image.';\n if(!shared){\n source=incoming;draft=incoming;sourceStamp=stamp;lastContent=draft;\n // Fixed canonical declarations keep keyed edits sparse. Legacy/custom rows\n // retain their exact shape through the SDK's checked atomic fallback.\n shared=StillMade.connectDocument({key:'boardDraft',source:incoming,collections:[{path:['strip'],key:'panelId',fallback:'atomic'},{path:['strip','*','elements'],key:'id',fallback:'atomic'}],read:()=>draft,replace:value=>{draft=value;revision+=1;past=null;future=null;lastContent=draft;draw();}});\n scratchShared=StillMade.connectDocument({key:'boardScratch',source:{text:null,base:null},read:()=>scratch,replace:value=>{scratch=value;renderRaw();syncControls();}});\n options={presenterPrompt:text(values.presenterPrompt)||'Friendly presenter explaining the narration'};\n optionsShared=StillMade.connectDocument({key:'boardOptions',source:clone(options),read:()=>options,replace:value=>{options=value;const input=document.getElementById('presenterPrompt');if(input.value!==options.presenterPrompt)input.value=options.presenterPrompt;}});\n controllers().forEach(item=>item.onStatus(()=>{draw();renderMediaLibrary();}));\n }else{\n if(stamp!==sourceStamp){revision+=1;past=null;future=null;sourceStamp=stamp;}\n source=incoming;shared.replaceSource(incoming);\n optionsShared.replaceSource({presenterPrompt:text(values.presenterPrompt)||'Friendly presenter explaining the narration'});\n }\n connectionError='';draw();renderMediaLibrary();renderWorkspaceSync(values.versions,values.presence);\n }catch(error){connectionError=error.message;setMessage(error.message);syncControls();}\n});\n"
|
|
199
|
+
},
|
|
200
|
+
"tests": [
|
|
201
|
+
{
|
|
202
|
+
"name": "Retain the complete current Whiteboard",
|
|
203
|
+
"input": {
|
|
204
|
+
"board": {
|
|
205
|
+
"schemaVersion": 1,
|
|
206
|
+
"meta": {
|
|
207
|
+
"aspect": "16:9"
|
|
208
|
+
},
|
|
209
|
+
"strip": [
|
|
210
|
+
{
|
|
211
|
+
"panelId": "board-1",
|
|
212
|
+
"type": "canvas",
|
|
213
|
+
"startMs": 0,
|
|
214
|
+
"durationMs": 5000,
|
|
215
|
+
"background": "#ffffff",
|
|
216
|
+
"elements": [
|
|
217
|
+
{
|
|
218
|
+
"id": "image-1",
|
|
219
|
+
"kind": "image",
|
|
220
|
+
"src": "https://media.example/retained.png",
|
|
221
|
+
"assetId": "asset-1",
|
|
222
|
+
"versionId": "version-2",
|
|
223
|
+
"custom": {
|
|
224
|
+
"keep": true
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
"updatedBoard": {
|
|
232
|
+
"schemaVersion": 1,
|
|
233
|
+
"strip": []
|
|
234
|
+
},
|
|
235
|
+
"commit": false
|
|
236
|
+
},
|
|
237
|
+
"expected": {
|
|
238
|
+
"board": {
|
|
239
|
+
"schemaVersion": 1,
|
|
240
|
+
"meta": {
|
|
241
|
+
"aspect": "16:9"
|
|
242
|
+
},
|
|
243
|
+
"strip": [
|
|
244
|
+
{
|
|
245
|
+
"panelId": "board-1",
|
|
246
|
+
"type": "canvas",
|
|
247
|
+
"startMs": 0,
|
|
248
|
+
"durationMs": 5000,
|
|
249
|
+
"background": "#ffffff",
|
|
250
|
+
"elements": [
|
|
251
|
+
{
|
|
252
|
+
"id": "image-1",
|
|
253
|
+
"kind": "image",
|
|
254
|
+
"src": "https://media.example/retained.png",
|
|
255
|
+
"assetId": "asset-1",
|
|
256
|
+
"versionId": "version-2",
|
|
257
|
+
"custom": {
|
|
258
|
+
"keep": true
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
"editorTimeline": {
|
|
266
|
+
"schemaVersion": 1,
|
|
267
|
+
"activeTimelineId": "whiteboard",
|
|
268
|
+
"clips": [
|
|
269
|
+
{
|
|
270
|
+
"id": "board-1",
|
|
271
|
+
"start": 0,
|
|
272
|
+
"duration": 5,
|
|
273
|
+
"canvas": {
|
|
274
|
+
"panelId": "board-1",
|
|
275
|
+
"type": "canvas",
|
|
276
|
+
"startMs": 0,
|
|
277
|
+
"durationMs": 5000,
|
|
278
|
+
"background": "#ffffff",
|
|
279
|
+
"elements": [
|
|
280
|
+
{
|
|
281
|
+
"id": "image-1",
|
|
282
|
+
"kind": "image",
|
|
283
|
+
"src": "https://media.example/retained.png",
|
|
284
|
+
"assetId": "asset-1",
|
|
285
|
+
"versionId": "version-2",
|
|
286
|
+
"custom": {
|
|
287
|
+
"keep": true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"audioItems": [],
|
|
295
|
+
"captions": [],
|
|
296
|
+
"textOverlays": [],
|
|
297
|
+
"effects": [],
|
|
298
|
+
"targetDuration": 5
|
|
299
|
+
},
|
|
300
|
+
"edit": {
|
|
301
|
+
"schemaVersion": 1,
|
|
302
|
+
"title": "Update Whiteboard (1 boards)",
|
|
303
|
+
"field": "board",
|
|
304
|
+
"before": {
|
|
305
|
+
"schemaVersion": 1,
|
|
306
|
+
"meta": {
|
|
307
|
+
"aspect": "16:9"
|
|
308
|
+
},
|
|
309
|
+
"strip": [
|
|
310
|
+
{
|
|
311
|
+
"panelId": "board-1",
|
|
312
|
+
"type": "canvas",
|
|
313
|
+
"startMs": 0,
|
|
314
|
+
"durationMs": 5000,
|
|
315
|
+
"background": "#ffffff",
|
|
316
|
+
"elements": [
|
|
317
|
+
{
|
|
318
|
+
"id": "image-1",
|
|
319
|
+
"kind": "image",
|
|
320
|
+
"src": "https://media.example/retained.png",
|
|
321
|
+
"assetId": "asset-1",
|
|
322
|
+
"versionId": "version-2",
|
|
323
|
+
"custom": {
|
|
324
|
+
"keep": true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
},
|
|
331
|
+
"after": {
|
|
332
|
+
"schemaVersion": 1,
|
|
333
|
+
"meta": {
|
|
334
|
+
"aspect": "16:9"
|
|
335
|
+
},
|
|
336
|
+
"strip": [
|
|
337
|
+
{
|
|
338
|
+
"panelId": "board-1",
|
|
339
|
+
"type": "canvas",
|
|
340
|
+
"startMs": 0,
|
|
341
|
+
"durationMs": 5000,
|
|
342
|
+
"background": "#ffffff",
|
|
343
|
+
"elements": [
|
|
344
|
+
{
|
|
345
|
+
"id": "image-1",
|
|
346
|
+
"kind": "image",
|
|
347
|
+
"src": "https://media.example/retained.png",
|
|
348
|
+
"assetId": "asset-1",
|
|
349
|
+
"versionId": "version-2",
|
|
350
|
+
"custom": {
|
|
351
|
+
"keep": true
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "Prepare one exact complete Whiteboard update",
|
|
363
|
+
"input": {
|
|
364
|
+
"board": {
|
|
365
|
+
"schemaVersion": 1,
|
|
366
|
+
"meta": {
|
|
367
|
+
"aspect": "16:9"
|
|
368
|
+
},
|
|
369
|
+
"strip": [
|
|
370
|
+
{
|
|
371
|
+
"panelId": "board-1",
|
|
372
|
+
"type": "canvas",
|
|
373
|
+
"startMs": 0,
|
|
374
|
+
"durationMs": 5000,
|
|
375
|
+
"transitionIn": "cut",
|
|
376
|
+
"elements": [
|
|
377
|
+
{
|
|
378
|
+
"id": "title-1",
|
|
379
|
+
"kind": "text",
|
|
380
|
+
"text": "Original",
|
|
381
|
+
"appearMs": 0
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
"updatedBoard": {
|
|
388
|
+
"schemaVersion": 1,
|
|
389
|
+
"meta": {
|
|
390
|
+
"aspect": "16:9"
|
|
391
|
+
},
|
|
392
|
+
"strip": [
|
|
393
|
+
{
|
|
394
|
+
"panelId": "board-1",
|
|
395
|
+
"type": "canvas",
|
|
396
|
+
"startMs": 0,
|
|
397
|
+
"durationMs": 6000,
|
|
398
|
+
"transitionIn": "fade",
|
|
399
|
+
"elements": [
|
|
400
|
+
{
|
|
401
|
+
"id": "title-1",
|
|
402
|
+
"kind": "text",
|
|
403
|
+
"text": "Updated",
|
|
404
|
+
"appearMs": 250
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
},
|
|
410
|
+
"commit": true
|
|
411
|
+
},
|
|
412
|
+
"expected": {
|
|
413
|
+
"board": {
|
|
414
|
+
"schemaVersion": 1,
|
|
415
|
+
"meta": {
|
|
416
|
+
"aspect": "16:9"
|
|
417
|
+
},
|
|
418
|
+
"strip": [
|
|
419
|
+
{
|
|
420
|
+
"panelId": "board-1",
|
|
421
|
+
"type": "canvas",
|
|
422
|
+
"startMs": 0,
|
|
423
|
+
"durationMs": 6000,
|
|
424
|
+
"transitionIn": "fade",
|
|
425
|
+
"elements": [
|
|
426
|
+
{
|
|
427
|
+
"id": "title-1",
|
|
428
|
+
"kind": "text",
|
|
429
|
+
"text": "Updated",
|
|
430
|
+
"appearMs": 250
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
"editorTimeline": {
|
|
437
|
+
"schemaVersion": 1,
|
|
438
|
+
"activeTimelineId": "whiteboard",
|
|
439
|
+
"clips": [
|
|
440
|
+
{
|
|
441
|
+
"id": "board-1",
|
|
442
|
+
"start": 0,
|
|
443
|
+
"duration": 6,
|
|
444
|
+
"canvas": {
|
|
445
|
+
"panelId": "board-1",
|
|
446
|
+
"type": "canvas",
|
|
447
|
+
"startMs": 0,
|
|
448
|
+
"durationMs": 6000,
|
|
449
|
+
"transitionIn": "fade",
|
|
450
|
+
"elements": [
|
|
451
|
+
{
|
|
452
|
+
"id": "title-1",
|
|
453
|
+
"kind": "text",
|
|
454
|
+
"text": "Updated",
|
|
455
|
+
"appearMs": 250
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
],
|
|
461
|
+
"audioItems": [],
|
|
462
|
+
"captions": [],
|
|
463
|
+
"textOverlays": [],
|
|
464
|
+
"effects": [],
|
|
465
|
+
"targetDuration": 6
|
|
466
|
+
},
|
|
467
|
+
"edit": {
|
|
468
|
+
"schemaVersion": 1,
|
|
469
|
+
"title": "Update Whiteboard (1 boards)",
|
|
470
|
+
"field": "board",
|
|
471
|
+
"before": {
|
|
472
|
+
"schemaVersion": 1,
|
|
473
|
+
"meta": {
|
|
474
|
+
"aspect": "16:9"
|
|
475
|
+
},
|
|
476
|
+
"strip": [
|
|
477
|
+
{
|
|
478
|
+
"panelId": "board-1",
|
|
479
|
+
"type": "canvas",
|
|
480
|
+
"startMs": 0,
|
|
481
|
+
"durationMs": 5000,
|
|
482
|
+
"transitionIn": "cut",
|
|
483
|
+
"elements": [
|
|
484
|
+
{
|
|
485
|
+
"id": "title-1",
|
|
486
|
+
"kind": "text",
|
|
487
|
+
"text": "Original",
|
|
488
|
+
"appearMs": 0
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
"after": {
|
|
495
|
+
"schemaVersion": 1,
|
|
496
|
+
"meta": {
|
|
497
|
+
"aspect": "16:9"
|
|
498
|
+
},
|
|
499
|
+
"strip": [
|
|
500
|
+
{
|
|
501
|
+
"panelId": "board-1",
|
|
502
|
+
"type": "canvas",
|
|
503
|
+
"startMs": 0,
|
|
504
|
+
"durationMs": 6000,
|
|
505
|
+
"transitionIn": "fade",
|
|
506
|
+
"elements": [
|
|
507
|
+
{
|
|
508
|
+
"id": "title-1",
|
|
509
|
+
"kind": "text",
|
|
510
|
+
"text": "Updated",
|
|
511
|
+
"appearMs": 250
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
],
|
|
521
|
+
"portable": {
|
|
522
|
+
"format": "stillmade-block/1",
|
|
523
|
+
"dependencies": [],
|
|
524
|
+
"sources": [
|
|
525
|
+
{
|
|
526
|
+
"kind": "original",
|
|
527
|
+
"license": "MIT",
|
|
528
|
+
"paths": [
|
|
529
|
+
"LICENSE",
|
|
530
|
+
"src/run.js",
|
|
531
|
+
"src/view.json"
|
|
532
|
+
],
|
|
533
|
+
"evidence": [
|
|
534
|
+
"upstream/0/LICENSE",
|
|
535
|
+
"upstream/0/src/run.js",
|
|
536
|
+
"upstream/0/src/view.json"
|
|
537
|
+
],
|
|
538
|
+
"changes": "Original implementation declared by the packager; no reused repository or dependency code."
|
|
539
|
+
}
|
|
540
|
+
],
|
|
541
|
+
"files": {
|
|
542
|
+
"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",
|
|
543
|
+
"upstream/0/src/run.js": "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.11'}},resultPort:null};\nreturn outputs;\n",
|
|
544
|
+
"upstream/0/src/view.json": "{\n \"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=\\\"undo\\\" data-stillmade-action=\\\"undo\\\" type=\\\"button\\\" class=\\\"secondary\\\">Undo</button><button id=\\\"redo\\\" data-stillmade-action=\\\"redo\\\" type=\\\"button\\\" class=\\\"secondary\\\">Redo</button><button id=\\\"reset\\\" data-stillmade-action=\\\"reset-changes\\\" type=\\\"button\\\" class=\\\"secondary\\\">Reset shared changes</button><button id=\\\"add\\\" data-stillmade-action=\\\"add-board\\\" 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 <output id=\\\"syncStatus\\\" role=\\\"status\\\" aria-live=\\\"polite\\\"></output>\\n <section id=\\\"recovery\\\" hidden><p>Your local draft is retained. Retry it, or discard only your unconfirmed edits.</p><div class=\\\"actions\\\"><button id=\\\"retry\\\" data-stillmade-action=\\\"retry-sharing\\\" type=\\\"button\\\" class=\\\"secondary\\\">Retry shared edits</button><button id=\\\"discard\\\" data-stillmade-action=\\\"discard-unsent-edits\\\" type=\\\"button\\\" class=\\\"secondary\\\">Discard unconfirmed edits</button></div><div id=\\\"sourceRecovery\\\" hidden><p>The project source changed. Resetting adopts that source and clears this shared Whiteboard draft for everyone.</p><label><input id=\\\"confirmSourceReset\\\" type=\\\"checkbox\\\">I understand this resets the shared draft for everyone.</label><button id=\\\"resetSource\\\" data-stillmade-action=\\\"use-project-source\\\" type=\\\"button\\\" class=\\\"secondary\\\">Use current project source</button></div></section>\\n <p id=\\\"sharedResults\\\" aria-live=\\\"polite\\\">No shared Whiteboard result yet.</p>\\n <section><h2>Saved revisions and collaborators</h2><div id=\\\"workspaceSync\\\"></div></section>\\n <section><h2>Project media library</h2><p>Browse exact stored image versions supplied by the host.</p><div id=\\\"mediaLibrary\\\"></div><img id=\\\"libraryPreview\\\" alt=\\\"Trusted project media preview\\\" hidden></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\\\" data-stillmade-action=\\\"add-connected-image\\\" type=\\\"button\\\" class=\\\"secondary\\\">Add connected image</button><button id=\\\"previewSelected\\\" data-stillmade-action=\\\"preview-image\\\" 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\\\" data-stillmade-action=\\\"prepare-editor\\\" type=\\\"button\\\" class=\\\"secondary\\\">Prepare Editor handoff</button></div><div class=\\\"playback\\\"><button id=\\\"advance\\\" data-stillmade-action=\\\"advance-preview\\\" 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 id=\\\"rawDetails\\\"><summary>Complete Whiteboard data</summary><p>Every field and placed media reference is retained. Unfinished JSON is shared separately; apply it before reviewing. Applying refuses a stale base rather than overwriting a teammate.</p><label for=\\\"raw\\\">Complete Whiteboard JSON</label><textarea id=\\\"raw\\\" class=\\\"raw\\\" spellcheck=\\\"false\\\"></textarea><div class=\\\"actions\\\"><button id=\\\"applyRaw\\\" data-stillmade-action=\\\"apply-json\\\" type=\\\"button\\\" class=\\\"secondary\\\">Apply JSON to workspace</button><button id=\\\"clearRaw\\\" data-stillmade-action=\\\"discard-json\\\" type=\\\"button\\\" class=\\\"secondary\\\">Discard shared JSON scratch</button></div></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\\\" data-stillmade-action=\\\"generate-presenter\\\" type=\\\"button\\\" class=\\\"secondary\\\">Generate presenter poses</button><button id=\\\"qualityReview\\\" data-stillmade-action=\\\"review-quality\\\" type=\\\"button\\\" class=\\\"secondary\\\">Review image quality</button><button id=\\\"renderFinal\\\" data-stillmade-action=\\\"render-video\\\" type=\\\"button\\\">Render final video</button></div></section></main>\\n\",\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\",\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,mediaAssets=[],boardPage=0;\\nlet shared=null,scratchShared=null,optionsShared=null,connectionError='',permission=StillMade.getShared().canEdit===true;\\nlet scratch={text:null,base:null},options={presenterPrompt:'Friendly presenter explaining the narration'},past=null,future=null,lastContent=null,sourceStamp='',rawDocument=null,rawWasScratch=false;\\nconst elementPages=new Map(),identityTokens=new Map(),tokenCache=new Map();\\nconst clone=value=>JSON.parse(JSON.stringify(value));\\nconst text=value=>value===undefined||value===null?'':String(value);\\nconst same=(a,b)=>a===b||JSON.stringify(a)===JSON.stringify(b);\\nconst nextId=prefix=>prefix+'-'+String(Date.now())+'-'+Math.random().toString(36).slice(2);\\nconst token=value=>{const valueText=JSON.stringify(value);if(tokenCache.has(valueText))return tokenCache.get(valueText);const key=Array.from(valueText).reduce((hash,char)=>(Math.imul(hash,31)+char.charCodeAt(0))>>>0,2166136261).toString(36)+'-'+Array.from(valueText).reduce((hash,char)=>(Math.imul(hash,131)+char.charCodeAt(0))>>>0,5381).toString(36);if(identityTokens.has(key)&&identityTokens.get(key)!==valueText)throw Error('Two control identities collide; use the complete JSON editor.');identityTokens.set(key,valueText);tokenCache.set(valueText,key);return key;};\\nconst boardKey=index=>{const board=draft.strip.at(index);return token(board&&board.panelId?['board',board.panelId,draft.strip.slice(0,index).filter(item=>item.panelId===board.panelId).length]:['legacy-board',index]);};\\nconst elementKey=(boardIndex,index)=>{const board=draft.strip.at(boardIndex),element=board&&Array.isArray(board.elements)?board.elements.at(index):null;return token(element&&element.id?['element',element.id,board.elements.slice(0,index).filter(item=>item.id===element.id).length]:['legacy-element',index]);};\\nconst controllers=()=>[shared,scratchShared,optionsShared].filter(Boolean);\\nconst canChange=()=>permission&&!busy&&!connectionError&&!!shared&&!controllers().some(item=>item.getStatus().error);\\nconst canRun=()=>canChange()&&scratch.text===null;\\nconst setMessage=value=>{status.textContent=value;};\\nconst field=(label,action,value,type='text',wide=false)=>({tag:'label',className:wide?'wide':'',children:[{tag:'span',text:label},type==='textarea'?{tag:'textarea',id:'field-'+action,action,value:text(value),disabled:!canChange(),label}:{tag:'input',id:'field-'+action,type,action,value:Number.isFinite(value)?value:text(value),disabled:!canChange(),label}]});\\nconst choice=(label,action,value,options)=>({tag:'label',children:[{tag:'span',text:label},{tag:'select',id:'field-'+action,action,value:text(value),disabled:!canChange(),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',id:'field-'+action,type:'checkbox',action,checked:!!value,disabled:!canChange(),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:!canChange()||disabled});\\nconst elementCard=(element,boardIndex,elementIndex)=>{const key=boardKey(boardIndex)+':'+elementKey(boardIndex,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',id:'element-'+key,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:!canChange()},{tag:'button',action:'remove-element:'+key,text:'Remove',className:'danger',disabled:!canChange()}]}]},{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),key=boardKey(index),elements=Array.isArray(board.elements)?board.elements:[],page=Math.min(elementPages.get(key)||0,Math.max(0,Math.ceil(elements.length/4)-1));elementPages.set(key,page);return {tag:'article',id:'board-'+key,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:'+key,'Up',position===0),moveButton('move-down:'+key,'Down',position===positions.length-1),{tag:'button',action:'add-text:'+key,text:'Add text',className:'secondary',disabled:!canChange()},{tag:'button',action:'remove-board:'+key,text:'Remove board',className:'danger',disabled:!canChange()||positions.length<=1}]}]},{tag:'div',className:'grid',children:[field('Board ID','board-id:'+key,board.panelId),field('Scene ID','board-scene:'+key,board.sceneId),field('Background','board-background:'+key,board.background),field('Starts (ms)','board-start:'+key,board.startMs,'number'),field('Length (ms)','board-duration:'+key,board.durationMs,'number'),choice('Transition','board-transition:'+key,board.transitionIn||'cut',optionList(board.transitionIn,['cut','fade','swirl','wipe','slide'])),field('Transition length (ms)','board-transition-ms:'+key,board.transitionMs,'number'),toggle('Automatic arrangement','board-arrange:'+key,board.autoArrange!==false)]},{tag:'div',className:'elements',children:Array.isArray(board.elements)&&board.elements.length?[{tag:'div',className:'row',children:[{tag:'button',action:'elements-back:'+key,text:'Previous elements',disabled:page===0},{tag:'span',text:'Elements '+String(page*4+1)+'–'+String(Math.min(elements.length,page*4+4))+' of '+String(elements.length)},{tag:'button',action:'elements-next:'+key,text:'Next elements',disabled:(page+1)*4>=elements.length}]},...elements.slice(page*4,page*4+4).map((element,elementIndex)=>elementCard(element,index,page*4+elementIndex))]:[{tag:'p',text:'No elements yet. Add text, request a presenter, or use Complete Whiteboard data for retained media.'}]}]}};\\nconst validBoard=value=>{if(!value||Array.isArray(value)||value.schemaVersion!==1||!Array.isArray(value.strip)||!value.strip.every(item=>item&&typeof item==='object'&&!Array.isArray(item)))throw Error('Whiteboard JSON must be a version 1 object with a strip of board rows.');return clone(value);};\\nconst renumber=()=>{let start=0;draft={...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 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-'+boardKey(index));if(frame)frame.classList.toggle('active',playheadMs>=(Number(board.startMs)||0)&&playheadMs<(Number(board.startMs)||0)+Math.max(1,Number(board.durationMs)||1));});};\\nconst syncControls=()=>{\\n const states=controllers().map(item=>item.getStatus()),error=connectionError||states.find(item=>item.error)?.error||'',sourceConflict=states.some(item=>item.sourceConflict),pending=states.some(item=>item.dirty||item.pending);\\n document.getElementById('syncStatus').textContent=error?'Shared edits paused. Your local draft is retained. '+error:!permission?'View only. Shared changes and results remain live.':pending?'Sharing changes…':'Shared draft is up to date locally; the project indicator reports saving.';\\n document.getElementById('recovery').hidden=!error;document.getElementById('sourceRecovery').hidden=!sourceConflict;\\n document.getElementById('retry').disabled=!permission||busy||sourceConflict||!error;\\n document.getElementById('discard').disabled=!permission||busy||sourceConflict||!error;\\n document.getElementById('resetSource').disabled=!permission||busy||!sourceConflict||!document.getElementById('confirmSourceReset').checked;\\n ['reset','add','applyRaw','clearRaw'].forEach(id=>{document.getElementById(id).disabled=!canChange();});\\n ['save','prepareEditor','generatePresenter','qualityReview','renderFinal'].forEach(id=>{document.getElementById(id).disabled=!canRun();});\\n document.getElementById('undo').disabled=!canChange()||!past;document.getElementById('redo').disabled=!canChange()||!future;\\n document.getElementById('addConnectedImage').disabled=!canChange()||!connectedImage;\\n raw.disabled=!canChange();aspect.disabled=!canChange();document.getElementById('presenterPrompt').disabled=!canChange();\\n};\\nconst formatRaw=()=>{if(!document.getElementById('rawDetails').hasAttribute('open')||scratch.text!==null)return;if(rawDocument!==draft||rawWasScratch){const value=JSON.stringify(draft,null,2);if(raw.value!==value)raw.value=value;rawDocument=draft;rawWasScratch=false;}};\\nconst renderRaw=()=>{if(scratch.text!==null){if(raw.value!==scratch.text)raw.value=scratch.text;rawWasScratch=true;return;}if(document.getElementById('rawDetails').hasAttribute('open')&&(rawDocument!==draft||rawWasScratch))StillMade.scheduleSharedEdit('boardRawPreview',formatRaw);};\\nconst draw=()=>{\\n const rows=draft.strip,positions=canvasPositions();boardPage=Math.min(boardPage,Math.max(0,Math.ceil(rows.length/2)-1));\\n StillMade.render('sequencePreview',rows.map((board,boardIndex)=>({board,boardIndex})).filter(item=>item.board.type==='canvas').slice(0,30).map(({board,boardIndex},index)=>({tag:'article',id:'preview-frame-'+boardKey(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 key=boardKey(boardIndex)+':'+elementKey(boardIndex,elementIndex),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-'+key,className:'spatial-item x-'+String(x)+' y-'+String(y)+sizeClass+(selectedElement===key?' selected':''),action:'select-element:'+key,text:text(element.text||element.descriptor||element.id||element.kind||'Element').slice(0,160)};})}]})));\\n updatePlayback();\\n StillMade.render('boards',rows.length?[{tag:'div',className:'row',children:[{tag:'button',action:'boards-back',text:'Previous boards',disabled:boardPage===0},{tag:'span',text:'Rows '+String(boardPage*2+1)+'–'+String(Math.min(rows.length,boardPage*2+2))+' of '+String(rows.length)},{tag:'button',action:'boards-next',text:'Next boards',disabled:(boardPage+1)*2>=rows.length}]},...rows.slice(boardPage*2,boardPage*2+2).map((board,index)=>boardCard(board,boardPage*2+index))]:[{tag:'p',text:'No boards yet. Add one to begin.'}]);\\n renderRaw();const aspectValue=text(draft.meta&&draft.meta.aspect);if(aspect.value!==aspectValue)aspect.value=aspectValue;\\n summary.textContent=String(positions.length)+' board'+(positions.length===1?'':'s')+' in this complete Whiteboard';syncControls();\\n};\\n// Mutation entry points copy only the touched object path. Immutable references\\n// keep exact undo bases without traversing the complete Whiteboard while typing.\\nconst contentChanged=()=>{revision+=1;if(lastContent&&lastContent!==draft){past={before:lastContent,after:draft};future=null;}lastContent=draft;shared.changed();renderRaw();setMessage('Changes are shared with the project. Review them to prepare a project update.');syncControls();};\\nconst flushDocuments=async()=>{await Promise.all(controllers().map(item=>item.flush()));};\\nconst renderMediaLibrary=()=>StillMade.render('mediaLibrary',mediaAssets.length?mediaAssets.slice(0,50).map((asset,index)=>({tag:'article',className:'card',children:[{tag:'strong',text:text(asset.title||asset.name||asset.assetId)+' · '+text(asset.versionId)},{tag:'div',className:'row',children:[{tag:'button',action:'library-preview:'+String(index),text:'Preview',className:'secondary'},{tag:'button',action:'library-attach:'+String(index),text:'Place on board',className:'secondary',disabled:!canChange()}]}]})):[{tag:'p',text:'No stored project images are available.'}]);\\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):'')}))}]);};\\nconst displayResults=outputs=>{const clips=outputs&&outputs.editorTimeline&&outputs.editorTimeline.clips,board=outputs&&outputs.board,actions=['presenterAction','qualityAction','renderAction'].filter(key=>Object.keys(outputs||{}).includes(key));document.getElementById('sharedResults').textContent=board?'Shared Whiteboard proposal · '+String(Array.isArray(board.strip)?board.strip.length:0)+' rows'+(Array.isArray(clips)?' · '+String(clips.length)+' Editor clips':'')+(actions.length?' · An action is ready for review below.':'')+' Your editable draft is not replaced by results.':'No shared Whiteboard result yet.';};\\nconst runDraft=async(mode,action)=>{\\n if(!canRun())return;busy=true;draw();renderMediaLibrary();setMessage('Sharing the latest draft before preparing the result…');\\n try{await flushDocuments();if(!permission||connectionError||controllers().some(item=>item.getStatus().error)||scratch.text!==null)throw Error('Editing access or the draft changed. Review the shared status before running.');\\n const currentRevision=revision,result=await StillMade.run({board:clone(source),updatedBoard:clone(draft),commit:true,...(action?{action}:{}),presenterPrompt:options.presenterPrompt});\\n // Results are proposals, never a replacement for a newer local/remote draft.\\n displayResults(StillMade.getShared().outputs||result);\\n if(currentRevision===revision)setMessage(mode==='editor'?String(result.editorTimeline.clips.length)+' Whiteboard clips are ready below for a connected Editor Step.':action?'The action is ready below for review.':'Whiteboard changes are ready below. Review the before and after, then choose Apply.');\\n }catch(error){setMessage(error.message||'The Whiteboard result could not be prepared.');}finally{busy=false;draw();renderMediaLibrary();}\\n};\\naspect.addEventListener('input',()=>{if(!canChange()){draw();return;}draft={...draft,meta:{...(!Array.isArray(draft.meta)&&draft.meta||{}),aspect:aspect.value}};contentChanged();});\\ndocument.getElementById('rawDetails').addEventListener('toggle',()=>{if(scratch.text===null)formatRaw();else renderRaw();});\\nraw.addEventListener('input',()=>{if(!canChange()){renderRaw();return;}scratch={text:raw.value,base:scratch.text===null?rawDocument||draft:scratch.base};rawWasScratch=true;scratchShared.changed();syncControls();});\\ndocument.getElementById('presenterPrompt').addEventListener('input',()=>{if(!canChange()){document.getElementById('presenterPrompt').value=options.presenterPrompt;return;}options.presenterPrompt=document.getElementById('presenterPrompt').value;optionsShared.changed();});\\ndocument.getElementById('applyRaw').addEventListener('click',async()=>{\\n if(!canChange()||scratch.text===null)return;busy=true;draw();\\n try{await scratchShared.flush();if(!permission||controllers().some(item=>item.getStatus().error))throw Error('Editing is unavailable; your JSON scratch is retained.');const pending=clone(scratch);if(!same(pending.base,draft))throw Error('The Whiteboard changed after this JSON draft began. Your scratch is retained; copy it, then discard the shared scratch to start from the latest board.');\\n const value=validBoard(JSON.parse(pending.text));draft=value;contentChanged();draw();await shared.flush();\\n if(!permission)throw Error('Editing access changed; the shared JSON scratch is retained.');\\n if(same(scratch,pending)){scratch={text:null,base:null};scratchShared.changed();await scratchShared.flush();}\\n setMessage('Complete Whiteboard data applied to the shared workspace.');\\n }catch(error){setMessage(error.message);}finally{busy=false;draw();}\\n});\\ndocument.getElementById('clearRaw').addEventListener('click',()=>{if(!canChange())return;scratch={text:null,base:null};scratchShared.changed();renderRaw();syncControls();});\\ndocument.getElementById('reset').addEventListener('click',()=>{if(!canChange())return;draft=source;contentChanged();draw();setMessage('Shared changes reset to the current project Whiteboard.');});\\ndocument.getElementById('undo').addEventListener('click',()=>{if(!canChange()||!past)return;const entry=past;if(draft!==entry.after){past=null;draw();return;}draft=entry.before;revision+=1;lastContent=draft;past=null;future=entry;shared.changed();draw();});\\ndocument.getElementById('redo').addEventListener('click',()=>{if(!canChange()||!future)return;const entry=future;if(draft!==entry.before){future=null;draw();return;}draft=entry.after;revision+=1;lastContent=draft;future=null;past=entry;shared.changed();draw();});\\ndocument.getElementById('add').addEventListener('click',()=>{if(!canChange())return;const start=draft.strip.filter(board=>board.type==='canvas').reduce((total,board)=>total+Math.max(0,Number(board.durationMs)||0),0);draft={...draft,strip:[...draft.strip,{panelId:nextId('portable-board'),type:'canvas',sceneId:null,background:'#ffffff',autoArrange:true,startMs:start,durationMs:5000,transitionIn:draft.strip.length?'swirl':'cut',elements:[]}]};boardPage=Math.floor((draft.strip.length-1)/2);contentChanged();draw();});\\nsave.addEventListener('click',()=>runDraft('review'));\\ndocument.getElementById('prepareEditor').addEventListener('click',()=>runDraft('editor'));\\ndocument.getElementById('generatePresenter').addEventListener('click',()=>runDraft('action','presenter'));\\ndocument.getElementById('qualityReview').addEventListener('click',()=>runDraft('action','quality'));\\ndocument.getElementById('renderFinal').addEventListener('click',()=>runDraft('action','render'));\\ndocument.getElementById('playhead').addEventListener('input',event=>{playheadMs=Number(event.target.value)||0;updatePlayback();});\\ndocument.getElementById('advance').addEventListener('click',()=>{const total=Number(document.getElementById('playhead').max)||1;playheadMs=playheadMs+500>=total?0:playheadMs+500;updatePlayback();});\\nconst recover=async(kind)=>{\\n if(!permission||busy)return;busy=true;draw();\\n try{if(kind==='source'){if(!document.getElementById('confirmSourceReset').checked)return;await shared.resetSharedSource({discardSharedEdits:true});document.getElementById('confirmSourceReset').checked=false;past=null;future=null;lastContent=draft;}\\n else await Promise.all(controllers().filter(item=>item.getStatus().error).map(item=>kind==='retry'?item.retry():item.discard()));\\n setMessage('Shared draft recovery completed.');\\n }catch(error){setMessage(error.message);}finally{busy=false;draw();renderMediaLibrary();}\\n};\\ndocument.getElementById('retry').addEventListener('click',()=>recover('retry'));\\ndocument.getElementById('discard').addEventListener('click',()=>recover('discard'));\\ndocument.getElementById('confirmSourceReset').addEventListener('change',syncControls);\\ndocument.getElementById('resetSource').addEventListener('click',()=>recover('source'));\\nStillMade.onAction(event=>{const parts=event.action.split(':'),kind=parts[0];if(kind==='boards-back'||kind==='boards-next'){boardPage=Math.max(0,boardPage+(kind==='boards-back'?-1:1));draw();return;}const boardIndex=draft.strip.findIndex((item,index)=>boardKey(index)===parts[1]);let board=boardIndex>=0?draft.strip.at(boardIndex):null;const elementIndex=board&&Array.isArray(board.elements)?board.elements.findIndex((item,index)=>elementKey(boardIndex,index)===parts[2]):-1;let element=elementIndex>=0?board.elements.at(elementIndex):null;if(kind==='elements-back'||kind==='elements-next'){elementPages.set(parts[1],Math.max(0,(elementPages.get(parts[1])||0)+(kind==='elements-back'?-1:1)));draw();return;}\\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=boardKey(boardIndex)+':'+elementKey(boardIndex,elementIndex);draw();status.textContent='Selected '+text(element.text||element.descriptor||element.id||element.kind||'element')+' in the spatial preview.';return}if(!canChange())return;if(kind==='remove-board'&&board&&board.type==='canvas'&&positions.length>1){draft={...draft,strip:draft.strip.filter((item,position)=>position!==boardIndex)};renumber();contentChanged();draw();return}if(moveTarget!==undefined&&board){const target=draft.strip.find((item,position)=>position===moveTarget);draft={...draft,strip:draft.strip.map((item,position)=>position===moveTarget?board:position===boardIndex?target:item)};renumber();contentChanged();draw();return}if(!board||board.type!=='canvas')return;\\n // Preserve exact undo/raw-scratch baselines: all subsequent field mutations\\n // target detached shallow paths, never a previously shared document object.\\n board={...board,...(Array.isArray(board.elements)?{elements:[...board.elements]}:{})};if(element){element={...element,...(element.pos&&typeof element.pos==='object'&&!Array.isArray(element.pos)?{pos:{...element.pos}}:{}),...(element.anim&&typeof element.anim==='object'&&!Array.isArray(element.anim)?{anim:{...element.anim}}:{})};board.elements=board.elements.map((item,index)=>index===elementIndex?element:item);}draft={...draft,strip:draft.strip.map((item,index)=>index===boardIndex?board:item)};\\n if(kind==='add-text'){if(!Array.isArray(board.elements))board.elements=[];board.elements=[...board.elements,{id:nextId('text'),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}];contentChanged();draw();return}if(kind==='remove-element'&&element){board.elements=board.elements.filter((item,position)=>position!==elementIndex);contentChanged();draw();return}if(kind==='duplicate-element'&&element){const copy=clone(element);copy.id=nextId(text(element.kind)||'element');board.elements=[...board.elements.slice(0,elementIndex+1),copy,...board.elements.slice(elementIndex+1)];contentChanged();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);contentChanged();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}contentChanged();if(kind==='element-kind'||kind==='element-id'||kind==='board-id')draw()});\\n\\nconst selected=()=>{const parts=selectedElement.split(':'),boardIndex=draft.strip.findIndex((item,index)=>boardKey(index)===parts[0]),board=boardIndex>=0?draft.strip.at(boardIndex):null,elementIndex=board&&Array.isArray(board.elements)?board.elements.findIndex((item,index)=>elementKey(boardIndex,index)===parts[1]):-1;return {boardIndex,board,element:elementIndex>=0?board.elements.at(elementIndex):null};};\\nconst attachImage=asset=>{if(!canChange())return;const picked=selected(),board=picked.board&&picked.board.type==='canvas'?picked.board:draft.strip.find(item=>item.type==='canvas');if(!board){setMessage('Add a board before placing media.');return;}const image={...clone(asset),asset:clone(asset),id:nextId('image'),kind:'image',src:asset.url,url:asset.url,pos:{x:.5,y:.5},size:.28,appearMs:0,leaveMs:null,pinned:false};const boardIndex=draft.strip.indexOf(board),elements=[...(Array.isArray(board.elements)?board.elements:[]),image];draft={...draft,strip:draft.strip.map((item,index)=>index===boardIndex?{...board,elements}:item)};selectedElement=boardKey(boardIndex)+':'+elementKey(boardIndex,elements.length-1);contentChanged();draw();setMessage('Exact stored image placed on the board.');};\\ndocument.getElementById('addConnectedImage').addEventListener('click',()=>{if(connectedImage)attachImage(connectedImage);});\\ndocument.getElementById('previewSelected').addEventListener('click',async()=>{const element=selected().element,reference=element&&element.asset&&element.asset.kind==='image'?element.asset:element;if(!reference||reference.kind!=='image'||!reference.url){setMessage('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;setMessage('Showing the trusted bounded preview for the selected image.');}catch(error){setMessage(error.message||'This image preview is unavailable.');}});\\nStillMade.onAction(async event=>{if(!event.action.startsWith('library-'))return;const parts=event.action.split(':'),asset=mediaAssets.find((item,index)=>index===Number(parts[1]));if(!asset)return;if(parts[0]==='library-preview'){try{const preview=await StillMade.previewImage(asset),image=document.getElementById('libraryPreview');image.src=preview.url;image.hidden=false;setMessage('Showing a trusted preview of the exact stored version.');}catch(error){setMessage(error.message||'Preview unavailable.');}return;}if(parts[0]==='library-attach')attachImage(asset);});\\nStillMade.onShared(snapshot=>{permission=snapshot.canEdit===true;displayResults(snapshot.outputs||{});draw();renderMediaLibrary();});\\nStillMade.onInput(values=>{\\n try{\\n const incoming=validBoard(values.board||{schemaVersion:1,strip:[]}),stamp=JSON.stringify(incoming);\\n connectedImage=values.generatedImage?clone(values.generatedImage):null;\\n mediaAssets=(Array.isArray(values.assets)?values.assets:[]).filter(asset=>asset&&asset.kind==='image'&&asset.url).slice(0,50);\\n document.getElementById('connectedMedia').textContent=connectedImage?'Ready: '+text(connectedImage.assetId)+' · '+text(connectedImage.versionId):'Connect Generate image or Import media to place an exact stored image.';\\n if(!shared){\\n source=incoming;draft=incoming;sourceStamp=stamp;lastContent=draft;\\n // Fixed canonical declarations keep keyed edits sparse. Legacy/custom rows\\n // retain their exact shape through the SDK's checked atomic fallback.\\n shared=StillMade.connectDocument({key:'boardDraft',source:incoming,collections:[{path:['strip'],key:'panelId',fallback:'atomic'},{path:['strip','*','elements'],key:'id',fallback:'atomic'}],read:()=>draft,replace:value=>{draft=value;revision+=1;past=null;future=null;lastContent=draft;draw();}});\\n scratchShared=StillMade.connectDocument({key:'boardScratch',source:{text:null,base:null},read:()=>scratch,replace:value=>{scratch=value;renderRaw();syncControls();}});\\n options={presenterPrompt:text(values.presenterPrompt)||'Friendly presenter explaining the narration'};\\n optionsShared=StillMade.connectDocument({key:'boardOptions',source:clone(options),read:()=>options,replace:value=>{options=value;const input=document.getElementById('presenterPrompt');if(input.value!==options.presenterPrompt)input.value=options.presenterPrompt;}});\\n controllers().forEach(item=>item.onStatus(()=>{draw();renderMediaLibrary();}));\\n }else{\\n if(stamp!==sourceStamp){revision+=1;past=null;future=null;sourceStamp=stamp;}\\n source=incoming;shared.replaceSource(incoming);\\n optionsShared.replaceSource({presenterPrompt:text(values.presenterPrompt)||'Friendly presenter explaining the narration'});\\n }\\n connectionError='';draw();renderMediaLibrary();renderWorkspaceSync(values.versions,values.presence);\\n }catch(error){connectionError=error.message;setMessage(error.message);syncControls();}\\n});\\n\"\n}"
|
|
545
|
+
},
|
|
546
|
+
"lineage": {
|
|
547
|
+
"parents": [],
|
|
548
|
+
"changes": "Packaged original Block source for StillMade."
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|