stillmade-sdk 0.1.8 → 0.1.9

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.
Files changed (53) hide show
  1. package/.stillmade/sdk-baseline.json +1 -1
  2. package/AGENTS.md +8 -2
  3. package/DESKTOP_CAPABILITIES.md +1 -1
  4. package/INTERFACE_DESIGN.md +167 -83
  5. package/MODULES.md +1 -1
  6. package/NATIVE_MCP.md +1 -1
  7. package/PASTE_TO_LLM.md +151 -12
  8. package/README.md +5 -2
  9. package/SDK.md +510 -115
  10. package/UI_AESTHETIC.md +64 -0
  11. package/blocks/native/sm.export-video/versions/2.1.0/package.js +580 -0
  12. package/blocks/native/sm.panel-strip/versions/2.4.0/package.js +1176 -0
  13. package/blocks/native/sm.panel-strip/versions/2.4.1/package.js +1316 -0
  14. package/blocks/native/sm.shot-plan/versions/2.2.0/package.js +800 -0
  15. package/blocks/native/sm.shot-plan/versions/2.2.1/package.js +920 -0
  16. package/examples/README.md +10 -0
  17. package/examples/custom-interface.stillmade.json +3 -3
  18. package/package.json +7 -1
  19. package/packages/block-cli/dev-host/assets/host.js +214 -28
  20. package/packages/block-cli/dev-host/assets/index.css +1 -1
  21. package/packages/block-cli/format.js +1 -1
  22. package/packages/block-cli/templates.js +53 -22
  23. package/packages/block-platform/block-readiness.js +5 -0
  24. package/packages/block-platform/brief-ports.js +29 -0
  25. package/packages/block-platform/catalog.js +32 -9
  26. package/packages/block-platform/current-semantic-source.js +7 -3
  27. package/packages/block-platform/flow-inspection.js +14 -4
  28. package/packages/block-platform/plain-descriptions.js +1 -1
  29. package/packages/block-platform/project-context.js +7 -0
  30. package/packages/block-platform/project-input-samples.js +3 -1
  31. package/packages/block-platform/project-inputs.js +11 -2
  32. package/packages/block-platform/project-start-input.js +46 -0
  33. package/packages/block-platform/step-groups.js +2 -0
  34. package/packages/block-platform/steps.js +55 -4
  35. package/packages/block-platform/type-preview.js +19 -3
  36. package/packages/block-platform/workspace-input-readset.js +6 -1
  37. package/packages/block-sdk/builder-contract.js +11 -5
  38. package/packages/block-sdk/collaboration-admission.js +4 -2
  39. package/packages/block-sdk/connection-adapters.js +35 -2
  40. package/packages/block-sdk/contracts.js +5 -2
  41. package/packages/block-sdk/examples.js +3 -3
  42. package/packages/block-sdk/first-party-identities.js +190 -0
  43. package/packages/block-sdk/first-party-packages.js +6 -1
  44. package/packages/block-sdk/index.d.ts +2 -0
  45. package/packages/block-sdk/index.js +1 -1
  46. package/packages/block-sdk/javascript.js +5 -1
  47. package/packages/block-sdk/platform-checks.js +13 -2
  48. package/packages/block-sdk/shared-agent-controls.js +5 -4
  49. package/packages/block-sdk/shared-controls.js +13 -9
  50. package/packages/block-sdk/view-design.js +104 -27
  51. package/packages/block-sdk/view.js +4 -2
  52. package/packages/design-system/block-kit.css +183 -0
  53. package/packages/design-system/block-kit.html +187 -0
@@ -33,6 +33,7 @@ import qualityAuditV102 from '../../blocks/native/sm.quality-audit/versions/1.0.
33
33
  import stockSearch from '../../blocks/native/sm.stock-search/versions/2.0.0/package.js';
34
34
  import youtubePublication from '../../blocks/native/sm.youtube/versions/2.0.0/package.js';
35
35
  import exportVideo from '../../blocks/native/sm.export-video/versions/2.0.0/package.js';
36
+ import exportVideoV210 from '../../blocks/native/sm.export-video/versions/2.1.0/package.js';
36
37
  import exportAudio from '../../blocks/native/sm.export-audio/versions/2.0.0/package.js';
37
38
  import motionGraphics from '../../blocks/native/sm.motion-graphics/versions/2.0.0/package.js';
38
39
  import audioScore from '../../blocks/native/sm.score/versions/2.0.0/package.js';
@@ -116,6 +117,8 @@ import shotPlanV210 from '../../blocks/native/sm.shot-plan/versions/2.1.0/packag
116
117
  import shotPlanV211 from '../../blocks/native/sm.shot-plan/versions/2.1.1/package.js';
117
118
  import shotPlanV212 from '../../blocks/native/sm.shot-plan/versions/2.1.2/package.js';
118
119
  import shotPlanV213 from '../../blocks/native/sm.shot-plan/versions/2.1.3/package.js';
120
+ import shotPlanV220 from '../../blocks/native/sm.shot-plan/versions/2.2.0/package.js';
121
+ import shotPlanV221 from '../../blocks/native/sm.shot-plan/versions/2.2.1/package.js';
119
122
  import boardEditor from '../../blocks/native/sm.board-editor/versions/2.0.0/package.js';
120
123
  import panelStripV200 from '../../blocks/native/sm.panel-strip/versions/2.0.0/package.js';
121
124
  import panelStrip from '../../blocks/native/sm.panel-strip/versions/2.1.0/package.js';
@@ -154,6 +157,8 @@ import panelStripV234 from '../../blocks/native/sm.panel-strip/versions/2.3.4/pa
154
157
  import panelStripV235 from '../../blocks/native/sm.panel-strip/versions/2.3.5/package.js';
155
158
  import panelStripV236 from '../../blocks/native/sm.panel-strip/versions/2.3.6/package.js';
156
159
  import panelStripV237 from '../../blocks/native/sm.panel-strip/versions/2.3.7/package.js';
160
+ import panelStripV240 from '../../blocks/native/sm.panel-strip/versions/2.4.0/package.js';
161
+ import panelStripV241 from '../../blocks/native/sm.panel-strip/versions/2.4.1/package.js';
157
162
  import boardEditorV210 from '../../blocks/native/sm.board-editor/versions/2.1.0/package.js';
158
163
  import boardEditorV220 from '../../blocks/native/sm.board-editor/versions/2.2.0/package.js';
159
164
  import boardEditorV221 from '../../blocks/native/sm.board-editor/versions/2.2.1/package.js';
@@ -181,5 +186,5 @@ import blueprintAssistant from '../../blocks/native/sm.blueprint-assistant/versi
181
186
  // Exact, independently executable first-party releases. Host workspaces do not
182
187
  // belong here. Changing implementation requires a new immutable version.
183
188
  const freeze=value=>{if(value&&typeof value==='object'&&!Object.isFrozen(value)){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
184
- export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(shortFormFinishV102),freeze(shortFormFinishV101),freeze(shortFormFinishV100),freeze(occasionStory),freeze(occasionStoryV101),freeze(occasionStoryV100),freeze(releaseVisuals),freeze(releaseVisualsV101),freeze(releaseVisualsV100),freeze(photoReelComposer),freeze(photoReelComposerV100),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV225),freeze(pipelineV224),freeze(blueprintV219),freeze(blueprintV218),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(chatV203),freeze(chatV202),freeze(chatV201),freeze(shotPlanV213),freeze(shotPlanV212),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(scriptV215),freeze(scriptV214),freeze(scriptV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(qualityAuditV102),freeze(qualityAuditV101),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(thumbnailV201),freeze(thumbnail),freeze(writeSummary),freeze(automationWhen),freeze(automationNewItems),freeze(automationAiDecide),freeze(automationContinueIf),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(script),freeze(scriptV210),freeze(scriptV211),freeze(scriptV212),freeze(generateVideoV201),freeze(generateVideo),freeze(chat),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(researchV202),freeze(researchV201),freeze(research),freeze(imageEditorV201),freeze(imageEditor),freeze(describeMediaV201),freeze(describeMedia),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(transcribeV201),freeze(transcribe),freeze(blueprint),freeze(blueprintV210),freeze(blueprintV211),freeze(blueprintV212),freeze(blueprintV213),freeze(blueprintV214),freeze(blueprintV215),freeze(blueprintV216),freeze(blueprintV217),freeze(shotPlan),freeze(shotPlanV210),freeze(shotPlanV211),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(animatedSceneV200),freeze(animatedScene),freeze(animatedSceneV220),freeze(animatedSceneV230),freeze(animatedSceneV231),freeze(animatedSceneV232),freeze(animatedSceneV233),freeze(animatedSceneV234),freeze(animatedSceneV235),freeze(animatedSceneV236),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222),freeze(shotPlanAssistant),freeze(animatedSceneAssistant),freeze(blueprintAssistant),freeze(whiteboardAssistant),freeze(qualityAudit)]);
189
+ export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(shortFormFinishV102),freeze(shortFormFinishV101),freeze(shortFormFinishV100),freeze(occasionStory),freeze(occasionStoryV101),freeze(occasionStoryV100),freeze(releaseVisuals),freeze(releaseVisualsV101),freeze(releaseVisualsV100),freeze(photoReelComposer),freeze(photoReelComposerV100),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV225),freeze(pipelineV224),freeze(blueprintV219),freeze(blueprintV218),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(chatV203),freeze(chatV202),freeze(chatV201),freeze(shotPlanV221),freeze(shotPlanV220),freeze(shotPlanV213),freeze(shotPlanV212),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(scriptV215),freeze(scriptV214),freeze(scriptV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(qualityAuditV102),freeze(qualityAuditV101),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(thumbnailV201),freeze(thumbnail),freeze(writeSummary),freeze(automationWhen),freeze(automationNewItems),freeze(automationAiDecide),freeze(automationContinueIf),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(script),freeze(scriptV210),freeze(scriptV211),freeze(scriptV212),freeze(generateVideoV201),freeze(generateVideo),freeze(chat),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(researchV202),freeze(researchV201),freeze(research),freeze(imageEditorV201),freeze(imageEditor),freeze(describeMediaV201),freeze(describeMedia),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(transcribeV201),freeze(transcribe),freeze(blueprint),freeze(blueprintV210),freeze(blueprintV211),freeze(blueprintV212),freeze(blueprintV213),freeze(blueprintV214),freeze(blueprintV215),freeze(blueprintV216),freeze(blueprintV217),freeze(shotPlan),freeze(shotPlanV210),freeze(shotPlanV211),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(animatedSceneV200),freeze(animatedScene),freeze(animatedSceneV220),freeze(animatedSceneV230),freeze(animatedSceneV231),freeze(animatedSceneV232),freeze(animatedSceneV233),freeze(animatedSceneV234),freeze(animatedSceneV235),freeze(animatedSceneV236),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222),freeze(shotPlanAssistant),freeze(animatedSceneAssistant),freeze(blueprintAssistant),freeze(whiteboardAssistant),freeze(qualityAudit)]);
185
190
  export const firstPartySdkPackage = manifest => FIRST_PARTY_SDK_PACKAGES.find(pkg => pkg.manifest.id === manifest?.id && pkg.manifest.version === manifest?.version);
@@ -62,6 +62,8 @@ export interface ConnectionAdapterPin { id: string; version: 1; }
62
62
  export interface ConnectionCompatibility { compatible: boolean; adapter?: ConnectionAdapterPin|null; requiresValue?: boolean; contextFields?: string[]; reason?: string; }
63
63
  export interface ConnectionAdapterOptions { semantics?: import('./semantic-contracts.js').SemanticRegistry; permissions?: string[]; adapter?: ConnectionAdapterPin|null; context?: Record<string, unknown>; }
64
64
  export const ADAPTERS: readonly Readonly<ConnectionAdapterPin & {output:string;input:string;contextFields:readonly string[];requiresValue:true}>[];
65
+ /** Narration conversions (script-as-narration, text-as-narration, transcript-as-narration-timing): valid only where a connection pins them. */
66
+ export const NARRATION_ADAPTERS: readonly Readonly<ConnectionAdapterPin & {output:string;input:string;targetSemantic:string;pinnedOnly:true;contextFields:readonly string[];requiresValue:true}>[];
65
67
  export function connectionCompatibility(output: string|Port, input: string|Port, options?: Pick<ConnectionAdapterOptions,'semantics'>): ConnectionCompatibility;
66
68
  export function validateConnectionAdapter(output: string|Port, input: string|Port, options?: Omit<ConnectionAdapterOptions,'context'>): ConnectionCompatibility;
67
69
  export function adaptConnectionValue(output: string|Port, input: string|Port, value: unknown, options?: ConnectionAdapterOptions): unknown;
@@ -26,7 +26,7 @@ export * from './admission.js';
26
26
  export {CONTEXT_FIELDS,resolveContextInputs,projectReadSnapshot} from './context.js';
27
27
 
28
28
  export {validateView,VIEW_LIMITS,FRAME_VIEW_LIMITS,FRAME_VIEW_ASSET_TYPES,isFrameView,validateFrameView,parseFrameViewJavaScript,instrumentFrameViewJavaScript,describeViewFiles} from './view.js';
29
- export {reviewViewDesign,INTERFACE_DESIGN_PRINCIPLES,INTERFACE_DESIGN_CONVENTIONS,INTERFACE_DESIGN_RULES,INTERFACE_AUTHORING_RULES} from './view-design.js';
29
+ export {reviewViewDesign,blockKitProblems,BLOCK_KIT_READINESS_RULES,BLOCK_INTERFACE_PRINCIPLES,BLOCK_KIT_CLASSES,INTERFACE_DESIGN_PRINCIPLES,INTERFACE_DESIGN_CONVENTIONS,INTERFACE_DESIGN_RULES,INTERFACE_AUTHORING_RULES} from './view-design.js';
30
30
 
31
31
  export {validateViewAppearance,APPEARANCE_TOKENS} from './appearance.js';
32
32
 
@@ -56,7 +56,11 @@ export async function executeJavaScript(pkg, values, {wasmLocation,state,signal,
56
56
  remainingCpu-=Math.max(0,now()-started);
57
57
  assert(remainingCpu>=0,'TIMEOUT','JavaScript exceeded its cumulative 500 ms execution limit');
58
58
  const guestError=result.error?vm.dump(result.error):null;
59
- assert(!result.error, interrupted ? 'TIMEOUT' : 'JAVASCRIPT', interrupted ? 'JavaScript exceeded its cumulative 500 ms execution limit' : `JavaScript failed${guestError?.message ? `: ${String(guestError.message).slice(0,1000)}` : '. Check its return values and memory limits.'}`);
59
+ // A plain Error the Block throws is its own message to the person (for
60
+ // example "Add playable timeline media before exporting."), shown as written.
61
+ // A runtime fault (TypeError, ReferenceError, ...) is a bug and says so.
62
+ const authored=guestError&&typeof guestError==='object'&&(guestError.name===undefined||guestError.name==='Error')&&typeof guestError.message==='string'&&guestError.message.trim()?guestError.message.trim().slice(0,1000):null;
63
+ assert(!result.error, interrupted ? 'TIMEOUT' : 'JAVASCRIPT', interrupted ? 'JavaScript exceeded its cumulative 500 ms execution limit' : authored || `JavaScript failed${guestError?.message ? `: ${String(guestError.message).slice(0,1000)}` : '. Check its return values and memory limits.'}`);
60
64
  assert(vm.typeof(result.value) === 'string', 'TYPE', 'Return declared outputs or a cooperative pending value');
61
65
  const json=vm.getString(result.value);serializedBytes+=new TextEncoder().encode(json).length;
62
66
  assert(serializedBytes<=JAVASCRIPT_LIMITS.jsonBytes,'LIMIT','JavaScript continuation and final outputs exceed the cumulative 4 MiB limit');
@@ -126,10 +126,21 @@ export function checkedPlatformTargets(report,expectedDigest) {
126
126
  return report.targets;
127
127
  }
128
128
 
129
+ // The surfaces activation waits for. StillMade's cloud checks cover phone and
130
+ // browser; the desktop runtime has its own runner, so a Block that also
131
+ // declares desktop activates on the surfaces the cloud checked and desktop stays
132
+ // unverified until a desktop report exists. A desktop-only Block still needs it.
133
+ export function activationTargets(manifest) {
134
+ const declared=manifest?.platforms;if(!declared)return ['phone','browser'];
135
+ const supported=Object.entries(declared).filter(([,value])=>value?.supported===true).map(([id])=>id),cloud=supported.filter(id=>id!=='desktop');
136
+ return cloud.length?cloud:supported;
137
+ }
138
+
129
139
  export function mobileActivationStatus(report,expectedDigest,manifest) {
130
140
  const targets=checkedPlatformTargets(report,expectedDigest);
131
- const declared=manifest?.platforms,required=declared?Object.entries(declared).filter(([,value])=>value?.supported===true).map(([id])=>id):['phone','browser'];
141
+ const declared=manifest?.platforms,required=activationTargets(manifest);
132
142
  if(!targets)return {ready:false,reason:'Run the current device UI profile for this exact package before activation.'};
133
143
  for(const id of required)if(targets[id]?.supported!==true)return {ready:false,reason:targets[id]?.reason||`The ${id} UI profile is not verified.`};
134
- return {ready:true,reason:declared?`Verified for ${required.join(', ')} use; excluded surfaces remain unavailable.`:'Compact, phone, tablet, landscape, and desktop web checks passed for this exact package.'};
144
+ const desktopPending=declared?.desktop?.supported===true&&!required.includes('desktop')&&targets.desktop?.supported!==true;
145
+ return {ready:true,reason:declared?`Verified for ${required.join(', ')} use; ${desktopPending?'desktop is verified separately by the desktop runner and ':''}excluded surfaces remain unavailable.`:'Compact, phone, tablet, landscape, and desktop web checks passed for this exact package.'};
135
146
  }
@@ -15,13 +15,14 @@ const label=value=>String(value||'').replace(/[-_]+/g,' ').replace(/\s+/g,' ').t
15
15
  // execute author JavaScript or attempt to infer fields from arbitrary code.
16
16
  export function sharedAgentControls(view){
17
17
  const html=String(view?.html||'').replace(/<!--[\s\S]*?-->/g,''),controls=[];
18
- for(const match of html.matchAll(/<(input|textarea|select)\b([^>]*)>/gi)){
18
+ for(const match of html.matchAll(/<(input|textarea|select|fieldset)\b([^>]*)>/gi)){
19
19
  const attrs=attributes(match[2]);
20
20
  if(!attrs.has('data-stillmade-share'))continue;
21
21
  const key=attrs.get('data-stillmade-share'),id=attrs.get('id'),element=match[1].toLowerCase(),inputType=(attrs.get('type')||'text').toLowerCase();
22
22
  if(!keyPattern.test(key||'')||!id||id.length>200)continue;
23
- const type=element==='input'?inputType:element;
24
- if(!['text','number','range','checkbox','color','textarea','select'].includes(type))continue;
23
+ // A shared fieldset holds radio picks; its value is the chosen option's value.
24
+ const type=element==='input'?(['search','email','tel','url'].includes(inputType)?'text':inputType):element==='fieldset'?'choice':element;
25
+ if(!['text','number','range','checkbox','color','textarea','select','choice'].includes(type))continue;
25
26
  const description=label(attrs.get('aria-label')||attrs.get('title')||key);
26
27
  controls.push({key,id,type,description,...(Number.isFinite(Number(attrs.get('min')))&&attrs.has('min')?{minimum:Number(attrs.get('min'))}:{}),...(Number.isFinite(Number(attrs.get('max')))&&attrs.has('max')?{maximum:Number(attrs.get('max'))}:{})});
27
28
  }
@@ -44,7 +45,7 @@ export function parseSharedAgentValue(control,current,value){
44
45
  return value;
45
46
  }
46
47
  if(control?.type==='color'){if(typeof value!=='string'||!/^#[0-9a-f]{6}$/i.test(value))throw new Error('This shared color needs a six-digit hex value.');return value;}
47
- if(['text','textarea','select'].includes(control?.type)){if(typeof value!=='string'||value.length>65536)throw new Error('This shared control needs bounded text.');return value;}
48
+ if(['text','textarea','select','choice'].includes(control?.type)){if(typeof value!=='string'||value.length>65536)throw new Error('This shared control needs bounded text.');return value;}
48
49
  if(current===null){if(value!==null)throw new Error('This dynamic shared field currently accepts null only.');return null;}
49
50
  if(Array.isArray(current)){if(!Array.isArray(value))throw new Error('Keep this dynamic shared field as an array.');return structuredClone(value);}
50
51
  if(plain(current)){if(!plain(value))throw new Error('Keep this dynamic shared field as an object.');return structuredClone(value);}
@@ -1,7 +1,7 @@
1
1
  // Platform-owned bindings. Serialized into the opaque frame; no author code is
2
2
  // replayed when a collaborator changes a value (particularly no run/click).
3
3
  export function installSharedControls(document,api,onError,textControls=null,lockedInputKeys=[]){
4
- const bindings=new Map();
4
+ const bindings=new Map(),TEXT_TYPES=['text','search','email','tel','url'];
5
5
  const lockedInputs=new Set(lockedInputKeys);
6
6
  const deferred=new Map(),running=new Set(),deferredErrors=new Map();let closed=false;
7
7
  const runDeferred=(key,entry)=>{
@@ -21,11 +21,13 @@ export function installSharedControls(document,api,onError,textControls=null,loc
21
21
  const bindShared=(key,id)=>{
22
22
  if(!/^[a-zA-Z][a-zA-Z0-9_-]{0,99}$/.test(key)||bindings.has(key))throw Error('Use one control per named shared field');
23
23
  const node=document.getElementById(id);
24
- if(!node||!['INPUT','TEXTAREA','SELECT'].includes(node.tagName)||node.tagName==='INPUT'&&!['text','number','range','checkbox','color'].includes(node.type))throw Error('Bind an existing text, number, checkbox or select control');
24
+ // A fieldset of radio buttons (the Block kit's sm-choices picks) shares the checked value.
25
+ const radios=node?.tagName==='FIELDSET'?[...node.querySelectorAll('input[type="radio"]')]:null;
26
+ if(!node||!['INPUT','TEXTAREA','SELECT','FIELDSET'].includes(node.tagName)||radios&&!radios.length||node.tagName==='INPUT'&&!['text','search','email','tel','url','number','range','checkbox','color'].includes(node.type))throw Error('Bind an existing text, number, checkbox or select control, or a fieldset of radio buttons');
25
27
  if(lockedInputs.has(key)){
26
28
  // The host owns a connected/context port. Its authored control may display
27
29
  // the value but must not save a competing shared draft under that port name.
28
- const textual=node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&node.type==='text';
30
+ const textual=node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&TEXT_TYPES.includes(node.type);
29
31
  const disabled=node.disabled,readOnly=node.readOnly,title=node.getAttribute('title'),ariaReadOnly=node.getAttribute('aria-readonly'),ariaDisabled=node.getAttribute('aria-disabled');
30
32
  if(textual){node.readOnly=true;node.setAttribute('aria-readonly','true');}
31
33
  else{node.disabled=true;node.setAttribute('aria-disabled','true');}
@@ -39,17 +41,18 @@ export function installSharedControls(document,api,onError,textControls=null,loc
39
41
  }};
40
42
  bindings.set(key,binding);return binding.dispose;
41
43
  }
42
- if(textControls&&(node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&node.type==='text')){
44
+ if(textControls&&(node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&TEXT_TYPES.includes(node.type))){
43
45
  const binding=textControls.bind(key,node),dispose=binding.dispose;binding.dispose=()=>{dispose();bindings.delete(key);};bindings.set(key,binding);return binding.dispose;
44
46
  }
45
47
  const originallyDisabled=node.disabled;
46
48
  let base={exists:false},draft,dirty=false,saving=false,timer=null,disposed=false,canEdit=false,inFlight=null,problem=null,dirtySince=null,composing=false,compositionRevoked=false,retainedComposition;
47
- const textual=node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&node.type==='text';
49
+ const textual=node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&TEXT_TYPES.includes(node.type);
48
50
  const report=error=>{problem=error;node.setCustomValidity(error.message);onError(error.message);};
49
51
  // A written value reaches the view through onShared (never an input event),
50
52
  // after the listeners of this update, whatever order they subscribed in.
51
- const write=value=>{const before=node.type==='checkbox'?node.checked:node.value;if(node.type==='checkbox')node.checked=!!value;else if(node.value!==String(value??''))node.value=String(value??'');if((node.type==='checkbox'?node.checked:node.value)!==before)queueMicrotask(()=>{if(!disposed)api.announce?.();});};
52
- const read=()=>node.type==='checkbox'?node.checked:['number','range'].includes(node.type)?node.valueAsNumber:node.value;
53
+ const picked=()=>radios.find(radio=>radio.checked)?.value??'';
54
+ const write=value=>{const before=radios?picked():node.type==='checkbox'?node.checked:node.value;if(radios)for(const radio of radios)radio.checked=radio.value===String(value??'');else if(node.type==='checkbox')node.checked=!!value;else if(node.value!==String(value??''))node.value=String(value??'');if((radios?picked():node.type==='checkbox'?node.checked:node.value)!==before)queueMicrotask(()=>{if(!disposed)api.announce?.();});};
55
+ const read=()=>radios?picked():node.type==='checkbox'?node.checked:['number','range'].includes(node.type)?node.valueAsNumber:node.value;
53
56
  let fallback=read();
54
57
  const flush=()=>{
55
58
  clearTimeout(timer);timer=null;if(saving)return inFlight;if(disposed||composing||!dirty||!canEdit||problem)return;
@@ -108,9 +111,10 @@ export function installSharedControls(document,api,onError,textControls=null,loc
108
111
  else if(!dirty&&!saving&&!problem)receive(api.getShared());
109
112
  };
110
113
  node.disabled=originallyDisabled||!api.getShared().canEdit;
111
- node.addEventListener('input',change);node.addEventListener('change',change);node.addEventListener('blur',flush);
114
+ const leave=radios?'focusout':'blur';
115
+ node.addEventListener('input',change);node.addEventListener('change',change);node.addEventListener(leave,flush);
112
116
  node.addEventListener('compositionstart',compositionStart);node.addEventListener('compositionend',compositionEnd);
113
- const dispose=()=>{disposed=true;composing=false;clearTimeout(timer);unsubscribe();node.removeEventListener('input',change);node.removeEventListener('change',change);node.removeEventListener('blur',flush);node.removeEventListener('compositionstart',compositionStart);node.removeEventListener('compositionend',compositionEnd);node.disabled=originallyDisabled;bindings.delete(key);};
117
+ const dispose=()=>{disposed=true;composing=false;clearTimeout(timer);unsubscribe();node.removeEventListener('input',change);node.removeEventListener('change',change);node.removeEventListener(leave,flush);node.removeEventListener('compositionstart',compositionStart);node.removeEventListener('compositionend',compositionEnd);node.disabled=originallyDisabled;bindings.delete(key);};
114
118
  bindings.set(key,{dispose,flush,status(){return {dirty,saving,problem,canEdit,composing,interrupted:retainedComposition!==undefined};},setDefault(value){fallback=value;receive(api.getShared());}});return dispose;
115
119
  };
116
120
  const bindAnnotated=()=>{
@@ -1,41 +1,118 @@
1
- // Conventions are guidance, not runtime security or admission guarantees.
2
- export const INTERFACE_DESIGN_CONVENTIONS = Object.freeze([
3
- 'Lead with the object being made, not its serialized source or transport.',
4
- 'Present one obvious primary action and keep peer choices to seven or fewer.',
5
- 'Use recognizable line icons for familiar actions, with accessible names and tooltips; never use emoji.',
6
- 'Prefer strong defaults and progressive disclosure over a wall of controls.',
7
- 'Keep labels short, preserve breathing room, and show status beside the action it describes.',
8
- 'Use the host step navigation only; do not repeat step counters or navigation inside the workspace.',
9
- 'Do not nest decorative panels or dialogs around the primary work surface.',
10
- 'Text scene descriptions are planning notes, not a visual storyboard or an approved frame.',
11
- ]);
12
- // Compatibility alias for existing consumers. These remain conventions.
13
- export const INTERFACE_DESIGN_PRINCIPLES = INTERFACE_DESIGN_CONVENTIONS;
14
- // Mandatory instructions to authors; not a claim of static enforcement.
15
- export const INTERFACE_AUTHORING_RULES = Object.freeze([
16
- 'Do not add technical results, JSON dumps, source editors, or diagnostic disclosures unless explicitly requested or part of an explicit source/debug task. Collapsed panels are not an exception.',
17
- 'Do not nest decorative cards, bordered panels, or modal shells around the same work area. Use spacing and labels within one workspace.',
18
- 'Do not duplicate host navigation, step counters, or workspace identity.',
19
- 'Show the useful output itself. Preserve required approvals, errors, progress, and stale-input warnings.',
20
- 'Keep ordinary Block pages and media inputs in normal scroll flow; do not introduce sticky media, headers, or clipped controls unless the editing task explicitly requires that layout.',
21
- 'Use purpose-labelled secondary task controls, not an ambiguous settings gear or hidden technical console.',
1
+ // How a Block interface should look and read. One ordered list, read top to
2
+ // bottom by people and models alike; docs/sdk/INTERFACE_DESIGN.md explains
3
+ // each item. Most of this is guidance: reviewViewDesign() turns a few of the
4
+ // items into warnings and one into an admission issue (INTERFACE_DESIGN_RULES).
5
+
6
+ // The default build-out. The host injects packages/design-system/block-kit.css
7
+ // into every Block view, so these classes work without shipping any CSS.
8
+ export const BLOCK_KIT_CSS_PATH='packages/design-system/block-kit.css';
9
+ export const BLOCK_KIT_REFERENCE_PATH='packages/design-system/block-kit.html';
10
+ export const BLOCK_KIT_CLASSES=Object.freeze({
11
+ 'sm-block':'The one workspace. data-layout="split" puts the stage beside the controls on wide screens.',
12
+ 'sm-stage':'The thing being made: preview, media or result. The biggest area; data-ratio="16:9|9:16|1:1".',
13
+ 'sm-empty':'What an empty stage shows: an .sm-glyph, at most five words and one action.',
14
+ 'sm-choices':'A fieldset of radio labels shown as pictures (img, .sm-swatch or .sm-glyph) and one or two words; replaces word dropdowns. data-compact for short text chips.',
15
+ 'sm-field':'A short label above one control. Group several in .sm-fields.',
16
+ 'sm-actions':'The action row: one .sm-primary, optional .sm-quiet buttons and .sm-icon buttons (aria-label and title required).',
17
+ 'sm-status':'Live state beside the action: data-state="loading|success|waiting|error" and a few words.',
18
+ 'sm-more':'A <details> for secondary settings; starts closed.',
19
+ 'sm-hint':'A <details> info toggle for an explanation that might help; starts closed.',
20
+ 'sm-gallery':'A grid of results or media.',
21
+ 'sm-glyph':'A small icon drawn in currentColor: data-glyph="play|pause|plus|close|check|chevron|upload|image|video|audio|text|spark|info|warning|spinner".',
22
+ });
23
+
24
+ // The design principles for every Block interface, in priority order.
25
+ export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
26
+ 'Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit.',
27
+ 'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
28
+ 'One obvious next action (`sm-primary`); peers are quiet buttons or named icons. Seven or fewer choices in view.',
29
+ 'The action button names what it does ("Write", "Generate image", "Make video"), never "Run", "Run step", "Submit" or "Review cost". Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher.',
30
+ 'Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji.',
31
+ 'Words only when needed. Drop what nobody needs to finish the task (intros, "how it works", restating the Block name). Words that might help start collapsed (`sm-hint`, `sm-more`). Errors, spending approvals, stale inputs and destructive steps stay visible, in few words.',
32
+ 'Strong defaults; secondary settings go in `sm-more`, closed.',
33
+ 'Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control.',
34
+ 'Status is a dot or a few words ("No narration yet"), never a paragraph; the reason sits behind an `sm-hint`.',
35
+ 'Files are tiles: a saved file is a thumbnail or chip and adding one is a + tile. Never the bare browser file picker, and no size limits in view until a file is too big.',
36
+ 'Rare tools (update, schedule, history, reload) are named icon buttons beside the action, never sections under the work.',
37
+ 'One workspace: no cards inside cards, no second title, step counter or Back/Next bar; the host owns navigation.',
38
+ 'Status sits beside the action it describes (`sm-status`), in a few words.',
39
+ 'Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them.',
40
+ 'Text scene descriptions are planning notes, never a storyboard or an approved frame.',
22
41
  ]);
23
- export const INTERFACE_DESIGN_RULES = Object.freeze([
24
- {id:'source-first-workspace',enforcement:'admission',description:'Reject a primary textarea of 12 or more rows without a detected task presentation.'},
42
+ // Compatibility names for existing consumers.
43
+ export const INTERFACE_DESIGN_CONVENTIONS=BLOCK_INTERFACE_PRINCIPLES;
44
+ export const INTERFACE_DESIGN_PRINCIPLES=BLOCK_INTERFACE_PRINCIPLES;
45
+ export const INTERFACE_AUTHORING_RULES=BLOCK_INTERFACE_PRINCIPLES;
46
+ export const INTERFACE_DESIGN_RULES=Object.freeze([
47
+ {id:'source-first-workspace',enforcement:'admission',description:'Reject a primary textarea of 12 or more rows without a detected task presentation.'},
25
48
  ]);
26
49
 
27
50
  const occurrences=(source,pattern)=>(source.match(pattern)||[]).length;
51
+ // A file input someone sees as the browser's own picker: not hidden and not inside a label tile.
52
+ // One forward scan with indexOf, so odd source never makes it slow.
53
+ function bareFilePicker(html){
54
+ const lower=html.toLowerCase();
55
+ for(let at=lower.indexOf('<input');at!==-1;at=lower.indexOf('<input',at+6)){
56
+ const end=lower.indexOf('>',at);if(end===-1)return false;
57
+ const tag=lower.slice(at,end);
58
+ if(!/\btype\s*=\s*["']?file\b/.test(tag)||/\bhidden\b|display\s*:\s*none|\bsm-sr\b/.test(tag))continue;
59
+ const label=lower.lastIndexOf('<label',at),closed=lower.lastIndexOf('</label>',at);
60
+ if(label===-1||closed>label)return true;
61
+ }
62
+ return false;
63
+ }
64
+ const words=text=>text.replace(/<[^>]*>/g,' ').trim().split(/\s+/).filter(Boolean).length;
28
65
 
29
66
  export function reviewViewDesign(view){
30
- if(!view)return {passed:true,issues:[],warnings:[],principles:INTERFACE_DESIGN_PRINCIPLES};
67
+ if(!view)return {passed:true,issues:[],warnings:[],principles:BLOCK_INTERFACE_PRINCIPLES};
31
68
  const html=view.html||'',javascript=view.javascript||'',primary=html.replace(/<details\b[\s\S]*?<\/details>/gi,'');
32
69
  const issues=[],warnings=[];
33
70
  const largeTextarea=/<textarea\b[^>]*rows\s*=\s*["']?(?:1[2-9]|[2-9]\d)["']?[^>]*>/i.test(primary);
34
- const visual=/<(?:canvas|img|video|figure|table|ol|ul)\b/i.test(primary)||/\bStillMade\.render\s*\(/.test(javascript);
71
+ const visual=/<(?:canvas|img|video|figure|table|ol|ul)\b/i.test(primary)||/\bsm-(?:stage|gallery)\b/.test(primary)||/\bStillMade\.render\s*\(/.test(javascript);
35
72
  if(largeTextarea&&!visual)issues.push('The primary workspace is dominated by serialized text. Show the thing being made first; include source or JSON only when explicitly requested or part of a source/debug task.');
36
73
  const controls=occurrences(primary,/<(?:button|input|select|textarea)\b/gi),options=occurrences(primary,/<option\b/gi);
37
- if(controls>8)warnings.push('More than eight controls are visible at once; group secondary controls behind progressive disclosure.');
74
+ if(controls>8)warnings.push('More than eight controls are visible at once; move secondary controls into a closed <details class="sm-more">.');
38
75
  if(options>7&&!/<optgroup\b/i.test(primary))warnings.push('A choice set exposes more than seven peers; use stronger defaults, groups, or a searchable picker.');
39
76
  if(occurrences(primary,/<h1\b/gi)>1)warnings.push('Use at most one workspace heading; do not repeat the Block identity in nested panels.');
40
- return {passed:issues.length===0,issues,warnings,principles:INTERFACE_DESIGN_PRINCIPLES};
77
+ const longText=[...primary.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/gi)].filter(match=>words(match[1])>20).length;
78
+ if(longText)warnings.push('A visible paragraph runs past twenty words. Drop words nobody needs; put helpful explanation in a closed <details class="sm-hint">.');
79
+ if(/<select\b/i.test(primary)&&/<(?:img|video|audio)\b/i.test(primary)===false&&/\b(?:voice|font|look|style|color|colour|template|theme|music|sound)\b/i.test(primary))warnings.push('People choose something they see or hear from a word list. Show each option as a picture or sample in <fieldset class="sm-choices">.');
80
+ // Compact: a bare browser file picker and printed size limits each take rows nobody needs.
81
+ if(bareFilePicker(primary))warnings.push('A bare file picker is in view. Show files as tiles: a + tile (a label around a hidden file input) and a thumbnail or chip for each saved file.');
82
+ if(/\bup to \d+\s*(?:mb|gb|kb)\b/i.test(primary.replace(/<[^<>]*>/g,' ')))warnings.push('A size limit is printed in view. Say it only when a file is too big.');
83
+ warnings.push(...blockKitProblems(view).map(problem=>problem.message));
84
+ return {passed:issues.length===0,issues,warnings,principles:BLOCK_INTERFACE_PRINCIPLES};
85
+ }
86
+
87
+ // What a NEW Block's interface must get right. Each problem is {rule, message}.
88
+ // The builder contract's DOD-15 (the SDK's `ready`, StillMade's builder) fails
89
+ // on 'action', 'price' and 'heading'; StillMade's own builder also repairs
90
+ // 'kit'. Installed Blocks are never refused for these; admission only warns.
91
+ const GENERIC_ACTIONS=/^(?:run|run step|run it|submit|go|start|ok|review cost|review price)$/i;
92
+ export const BLOCK_KIT_READINESS_RULES=Object.freeze(['action','price','heading']);
93
+ export function blockKitProblems(view){
94
+ if(!view)return [];
95
+ const problems=[],html=String(view.html||''),javascript=String(view.javascript||''),original=view.appearance==='original';
96
+ // One linear pass over the code: the text inside quotes (regular expressions here
97
+ // could retry at every position on unterminated quotes).
98
+ let strings='',quote=null;
99
+ for(let index=0;index<javascript.length;index++){
100
+ const char=javascript[index];
101
+ if(!quote){if(char==='"'||char==="'"||char==='`'){quote=char;strings+='\n';}continue;}
102
+ if(char==='\\'){index++;continue;}
103
+ if(char===quote)quote=null;else strings+=char;
104
+ }
105
+ const lower=html.toLowerCase();
106
+ // Each character is read a bounded number of times: the next search starts after this button.
107
+ for(let start=lower.indexOf('<button'),close=0;start!==-1;start=lower.indexOf('<button',close+9)){
108
+ const open=lower.indexOf('>',start);close=open===-1?-1:lower.indexOf('</button>',open);
109
+ if(close===-1)break;
110
+ const label=html.slice(open+1,close).replace(/<[^<>]*>/g,' ').replace(/\s+/g,' ').trim();
111
+ if(GENERIC_ACTIONS.test(label)){problems.push({rule:'action',message:`A button says "${label}". Name what it does ("Write", "Generate image"), never "Run" or "Submit".`});break;}
112
+ }
113
+ const visible=html.replace(/<[^<>]*>/g,' ')+'\n'+strings;
114
+ if(/\b\d+(?:\.\d+)?\s*credits?\b|\}\s*credits?\b|\$\s?\d/i.test(visible))problems.push({rule:'price',message:'The interface shows a price. Show no credit numbers; StillMade puts the price on its own run button.'});
115
+ if(!original&&/<h1\b/i.test(html))problems.push({rule:'heading',message:'Drop the page heading; StillMade already shows the Block\'s name.'});
116
+ if(!original&&view.runtime!=='frame'&&!/\bclass\s*=\s*["'][^"']*\bsm-block\b/i.test(html))problems.push({rule:'kit',message:'Build the interface from the Block kit: one <main class="sm-block"> with an sm-stage for the result and sm-actions for the button.'});
117
+ return problems;
41
118
  }
@@ -10,6 +10,8 @@ const forbiddenProperties=new Set([...forbidden,'assign','values','entries','fro
10
10
  // Exported so the SDK docs list exactly what the validator rejects.
11
11
  export const VIEW_BLOCKED_NAMES=Object.freeze([...forbidden]);
12
12
  export const VIEW_BLOCKED_PROPERTIES=Object.freeze(['assign','values','entries','fromEntries']);
13
+ // Syntax standard view code may not contain (loops and function declarations: views iterate with array methods).
14
+ export const VIEW_UNSUPPORTED_SYNTAX=Object.freeze(['ImportExpression','ImportDeclaration','WithStatement','ThisExpression','MetaProperty','WhileStatement','DoWhileStatement','ForStatement','ForInStatement','ForOfStatement','FunctionDeclaration']);
13
15
  const tags=new Set(['div','section','article','header','footer','main','aside','nav','h1','h2','h3','h4','p','span','strong','em','i','small','br','hr','label','input','textarea','button','select','option','optgroup','fieldset','legend','output','progress','meter','ul','ol','li','dl','dt','dd','figure','figcaption','img','video','audio','source','canvas','table','thead','tbody','tfoot','tr','th','td','pre','code','details','summary']);
14
16
  export const VIEW_TAGS=Object.freeze([...tags]);
15
17
  const propertyName=node=>(node.property||node.key)?.name??(node.property||node.key)?.value;
@@ -87,12 +89,12 @@ export function validateView(view){
87
89
  let ast;try{ast=parse(`"use strict";\n${view.javascript}`,{ecmaVersion:2022,sourceType:'script'});}catch(error){throw Object.assign(new Error(`View JavaScript syntax: ${error.message}`),{code:'VIEW'});}
88
90
  const {nodes,parents,allowed}=promptDataAccess(ast),functions=new Map();
89
91
  for(const node of nodes){
90
- assert(!['ImportExpression','ImportDeclaration','WithStatement','ThisExpression','MetaProperty','WhileStatement','DoWhileStatement','ForStatement','ForInStatement','ForOfStatement','FunctionDeclaration'].includes(node.type),'VIEW',`Unsupported view syntax: ${node.type}`);
92
+ assert(!VIEW_UNSUPPORTED_SYNTAX.includes(node.type),'VIEW',`Unsupported view syntax: ${node.type}`);
91
93
  if(node.type==='FunctionExpression')assert(!node.id,'VIEW','Named recursive functions are unsupported in view code');
92
94
  if(node.type==='VariableDeclarator' && node.id?.type==='Identifier' && ['FunctionExpression','ArrowFunctionExpression'].includes(node.init?.type))functions.set(node.id.name,node.init);
93
95
  if(node.type==='CallExpression' && node.callee?.type==='Identifier')assert(!['addEventListener','removeEventListener','dispatchEvent'].includes(node.callee.name),'VIEW','Attach events to declared controls, not the global frame');
94
96
  const parent=parents.get(node),dataKey=allowed.has(parent)&&(parent.property||parent.key)===node;
95
- if(node.type==='Identifier')assert(dataKey||!forbidden.has(node.name),'VIEW',`Unsupported view API: ${node.name}`);
97
+ if(node.type==='Identifier')assert(dataKey||!forbidden.has(node.name),'VIEW',`Unsupported view API: ${node.name}. View code cannot use this name, even for its own variable; rename it.`);
96
98
  if(['MemberExpression','Property','MethodDefinition'].includes(node.type) && !node.computed)assert(allowed.has(node)||!forbiddenProperties.has(propertyName(node)),'VIEW','Unsupported view property');
97
99
  if(['MemberExpression','Property','MethodDefinition'].includes(node.type) && node.computed){
98
100
  const key=node.property || node.key;
@@ -0,0 +1,183 @@
1
+ /* StillMade Block kit: the default build-out for every Block interface.
2
+ The host injects this file into every Block view (standard and frame), after
3
+ the theme and before the Block's own CSS, so any Block can use these classes
4
+ without shipping styles. Every rule sits inside :where() so a Block's own
5
+ stylesheet always wins when a person explicitly asks for a different look.
6
+
7
+ The shape of a Block page, top to bottom:
8
+ .sm-block one workspace, no cards inside cards
9
+ .sm-stage the thing being made (preview, media, result); biggest area
10
+ .sm-choices picks shown as pictures or icons, not word lists
11
+ .sm-shape a frame shape at its real ratio (style="--r:9/16")
12
+ .sm-field the few inputs a person actually needs
13
+ .sm-actions one .sm-primary, optional .sm-quiet / .sm-icon
14
+ .sm-status short live state beside the action
15
+ .sm-more everything secondary, collapsed
16
+ .sm-hint an info toggle for words that might help, collapsed
17
+ .sm-empty what an empty stage shows: a glyph, a few words, one action
18
+ .sm-glyph icons drawn in CSS with currentColor (no SVG or url needed)
19
+
20
+ The reference page is packages/design-system/block-kit.html. Keep this file
21
+ free of url(), @import and @font-face: Block views refuse to load resources. */
22
+
23
+ /* ---------- Layout ---------- */
24
+ :where(html:has(.sm-block)){container-type:inline-size}
25
+ :where(.sm-block){display:grid;gap:var(--s-5,24px);min-width:0}
26
+ :where(.sm-block,.sm-block *){box-sizing:border-box}
27
+ :where(.sm-block) > :where(*){min-width:0}
28
+ /* Unnamed container queries read the view page (the host makes it a container). */
29
+ @container (min-width:720px){
30
+ :where(.sm-block[data-layout="split"]){grid-template-columns:minmax(0,1.6fr) minmax(260px,1fr);align-items:start}
31
+ :where(.sm-block[data-layout="split"]) > :where(.sm-stage){grid-row:span 6}
32
+ }
33
+ :where(.sm-row){display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2,8px)}
34
+
35
+ /* ---------- The stage: the object being made gets the main surface ---------- */
36
+ :where(.sm-stage){position:relative;display:grid;place-items:center;min-height:220px;padding:var(--s-3,12px);border-radius:var(--r-3,8px);background:var(--bg-inset);overflow:hidden}
37
+ :where(.sm-stage[data-ratio="16:9"]){aspect-ratio:16/9;min-height:0}
38
+ :where(.sm-stage[data-ratio="9:16"]){aspect-ratio:9/16;min-height:0;max-height:70vh;justify-self:center;width:auto;height:min(70vh,640px)}
39
+ :where(.sm-stage[data-ratio="1:1"]){aspect-ratio:1;min-height:0}
40
+ :where(.sm-stage) > :where(img,video,canvas){display:block;max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--r-2,4px)}
41
+ :where(.sm-stage[data-fill]) > :where(img,video,canvas){width:100%;height:100%;object-fit:cover}
42
+
43
+ /* ---------- Empty state: a picture and a few words, never a paragraph ---------- */
44
+ :where(.sm-empty){display:grid;justify-items:center;gap:var(--s-3,12px);padding:var(--s-5,24px) var(--s-4,16px);text-align:center;color:var(--fg-muted)}
45
+ :where(.sm-empty) > :where(.sm-glyph){--sm-glyph-size:40px;color:var(--fg-subtle)}
46
+ :where(.sm-empty) > :where(p){margin:0;max-width:24ch;font-size:var(--fs-body,1rem)}
47
+ :where(.sm-drop){outline:1px dashed var(--border-tok);outline-offset:-8px}
48
+ :where(.sm-drop[data-over]){outline-color:var(--fg);background:var(--hover-wash)}
49
+
50
+ /* ---------- Actions ---------- */
51
+ :where(.sm-actions){display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2,8px)}
52
+ :where(.sm-actions) > :where(.sm-status){margin-inline-start:var(--s-2,8px)}
53
+ :where(.sm-primary,.sm-quiet){display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2,8px);min-height:var(--touch-target,44px);padding:10px 20px;border-radius:var(--r-pill,999px);font:inherit;font-weight:500;cursor:pointer;max-width:100%;text-align:center;overflow-wrap:anywhere}
54
+ :where(.sm-primary){border:1px solid var(--fg);background:var(--fg);color:var(--bg)}
55
+ :where(.sm-quiet){border:1px solid var(--border-tok);background:transparent;color:var(--fg)}
56
+ :where(.sm-quiet):hover{background:var(--hover-wash)}
57
+ :where(.sm-icon){display:inline-grid;place-items:center;min-width:var(--touch-target,44px);min-height:var(--touch-target,44px);padding:0;border:0;border-radius:var(--r-2,4px);background:transparent;color:var(--fg-muted);cursor:pointer}
58
+ :where(.sm-icon):hover{color:var(--fg);background:var(--hover-wash)}
59
+ :where(.sm-icon[aria-pressed="true"]){color:var(--fg);background:var(--press-wash)}
60
+ :where(.sm-primary,.sm-quiet,.sm-icon):disabled{opacity:.45;cursor:default}
61
+ :where(.sm-primary,.sm-quiet,.sm-icon):focus-visible{outline:2px solid var(--signal);outline-offset:3px}
62
+ @container (max-width:420px){
63
+ :where(.sm-actions) > :where(.sm-primary){flex:1 1 100%}
64
+ }
65
+
66
+ /* ---------- Fields: a short label above one control ---------- */
67
+ :where(.sm-field){display:grid;gap:6px;min-width:0}
68
+ :where(.sm-field) > :where(label,.sm-label){display:flex;align-items:center;gap:var(--s-1,4px);font-size:var(--fs-body-s,.875rem);color:var(--fg-muted);min-height:24px}
69
+ :where(.sm-field) :where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),select,textarea){width:100%;min-height:var(--touch-target,44px);padding:10px 12px;border:1px solid var(--border-tok);border-radius:var(--r-3,8px);background:var(--bg-elev-1);color:var(--fg);font:inherit}
70
+ :where(.sm-field) :where(textarea){min-height:96px;resize:vertical}
71
+ :where(.sm-fields){display:grid;gap:var(--s-4,16px);grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr))}
72
+ /* Compact: on a view wider than a phone a short field is one row, its label beside the control. */
73
+ @container (min-width:520px){
74
+ :where(.sm-field:not(:has(textarea))){grid-template-columns:minmax(5rem,9rem) minmax(0,1fr);align-items:center;gap:var(--s-2,8px)}
75
+ :where(.sm-field) :where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),select){min-height:36px;padding:6px 10px}
76
+ :where(.sm-fields){gap:var(--s-2,8px) var(--s-4,16px)}
77
+ }
78
+
79
+ /* ---------- Choices as pictures: replaces word dropdowns and radio lists ---------- */
80
+ :where(.sm-choices){display:grid;gap:var(--s-2,8px);grid-template-columns:repeat(auto-fill,minmax(var(--sm-tile,96px),1fr));margin:0;padding:0;border:0;min-width:0}
81
+ :where(.sm-choices[data-size="wide"]){--sm-tile:160px}
82
+ :where(.sm-choices) > :where(legend){padding:0;margin-bottom:6px;font-size:var(--fs-body-s,.875rem);color:var(--fg-muted)}
83
+ :where(.sm-choices) > :where(label){position:relative;display:grid;justify-items:center;align-content:start;gap:6px;padding:var(--s-2,8px);border:1px solid var(--border-tok);border-radius:var(--r-3,8px);background:var(--bg-elev-1);color:var(--fg-muted);font-size:var(--fs-caption,.8125rem);text-align:center;cursor:pointer}
84
+ :where(.sm-choices) > :where(label):hover{border-color:var(--border-strong);color:var(--fg)}
85
+ :where(.sm-choices) :where(input[type="radio"],input[type="checkbox"]){position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
86
+ :where(.sm-choices) > :where(label:has(input:checked)){border-color:var(--fg);color:var(--fg);box-shadow:inset 0 0 0 1px var(--fg)}
87
+ :where(.sm-choices) > :where(label:has(input:focus-visible)){outline:2px solid var(--signal);outline-offset:2px}
88
+ :where(.sm-choices) :where(img,video,canvas,.sm-swatch){display:block;width:100%;aspect-ratio:var(--sm-tile-ratio,1);object-fit:cover;border-radius:var(--r-2,4px);background:var(--bg-inset)}
89
+ :where(.sm-choices) :where(.sm-glyph){--sm-glyph-size:28px;margin:10px 0 4px}
90
+ :where(.sm-choices[data-ratio="16:9"]){--sm-tile-ratio:16/9}
91
+ :where(.sm-choices[data-ratio="9:16"]){--sm-tile-ratio:9/16}
92
+ :where(.sm-choices[data-compact]) > :where(label){grid-auto-flow:column;justify-content:center;align-content:center;align-items:center;min-height:var(--touch-target,44px);padding:6px 12px}
93
+ :where(.sm-choices[data-compact]) :where(.sm-glyph){--sm-glyph-size:18px;margin:0}
94
+ :where(.sm-choices) :where(audio){width:100%;height:32px;position:relative;z-index:1}
95
+ /* A frame shape drawn at its real proportion: <span class="sm-shape" style="--r:9/16"></span>. */
96
+ :where(.sm-shape){display:block;box-sizing:border-box;width:calc(var(--sm-shape-size,32px) * min(1,var(--r,1)));aspect-ratio:var(--r,1);max-width:100%;margin:4px auto;border:2px solid currentColor;border-radius:3px}
97
+ :where(.sm-choices) > :where(label:has(.sm-shape)){grid-template-rows:1fr auto;align-content:stretch}
98
+ :where(.sm-choices) :where(.sm-shape){--sm-shape-size:36px;align-self:center;margin:6px auto 2px}
99
+ :where(.sm-choices[data-compact]) :where(.sm-shape){--sm-shape-size:20px;margin:0}
100
+
101
+ /* ---------- Media gallery ---------- */
102
+ :where(.sm-gallery){display:grid;gap:var(--s-2,8px);grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sm-tile,140px)),1fr));margin:0;padding:0;list-style:none}
103
+ :where(.sm-gallery) > :where(*){position:relative;margin:0;border-radius:var(--r-3,8px);overflow:hidden;background:var(--bg-inset)}
104
+ :where(.sm-gallery) :where(img,video,canvas){display:block;width:100%;aspect-ratio:var(--sm-tile-ratio,1);object-fit:cover}
105
+ :where(.sm-gallery) :where(figcaption){padding:6px 8px;font-size:var(--fs-caption,.8125rem);color:var(--fg-muted)}
106
+ :where(.sm-gallery) :where([aria-selected="true"],.is-selected){box-shadow:inset 0 0 0 2px var(--fg)}
107
+
108
+ /* ---------- Status: a glyph and a few words, next to the action ---------- */
109
+ :where(.sm-status){display:inline-flex;align-items:center;gap:var(--s-2,8px);min-height:24px;font-size:var(--fs-body-s,.875rem);color:var(--fg-muted)}
110
+ :where(.sm-status):empty{display:none}
111
+ :where(.sm-status[data-state])::before{content:"";flex:none;width:14px;height:14px;border-radius:50%;background:currentColor}
112
+ :where(.sm-status[data-state="loading"])::before{background:transparent;border:2px solid currentColor;border-right-color:transparent;animation:sm-spin .8s linear infinite}
113
+ :where(.sm-status[data-state="success"]){color:var(--attention-ok,var(--fg))}
114
+ :where(.sm-status[data-state="success"])::before{border-radius:0;background:transparent;width:6px;height:11px;margin:0 4px 3px;border:solid currentColor;border-width:0 2px 2px 0;transform:rotate(45deg)}
115
+ :where(.sm-status[data-state="waiting"]){color:var(--attention-warning,var(--signal))}
116
+ :where(.sm-status[data-state="waiting"])::before{width:8px;height:8px;margin:0 3px}
117
+ :where(.sm-status[data-state="error"]){color:var(--attention-error,var(--signal))}
118
+ :where(.sm-status[data-state="error"])::before{border-radius:0;clip-path:polygon(50% 0,100% 100%,0 100%)}
119
+ :where(.sm-progress){width:100%;height:4px;border:0;border-radius:var(--r-pill,999px);overflow:hidden;background:var(--bg-elev-2);accent-color:var(--fg);appearance:none}
120
+ :where(.sm-progress)::-webkit-progress-bar{background:var(--bg-elev-2)}
121
+ :where(.sm-progress)::-webkit-progress-value{background:var(--fg)}
122
+ :where(.sm-progress)::-moz-progress-bar{background:var(--fg)}
123
+
124
+ /* ---------- Collapsed words: More (secondary settings) and hints ---------- */
125
+ :where(.sm-more){border-top:1px solid var(--divider)}
126
+ :where(.sm-more) > :where(summary){display:flex;align-items:center;gap:var(--s-2,8px);min-height:var(--touch-target,44px);padding:0;list-style:none;color:var(--fg-muted);font-size:var(--fs-body-s,.875rem);cursor:pointer}
127
+ :where(.sm-more) > :where(summary)::-webkit-details-marker{display:none}
128
+ :where(.sm-more) > :where(summary)::after{content:"";width:7px;height:7px;margin-inline-start:2px;border:solid currentColor;border-width:0 1.5px 1.5px 0;transform:translateY(-2px) rotate(45deg);transition:transform var(--dur-fast,160ms) var(--ease-out,ease)}
129
+ :where(.sm-more[open]) > :where(summary)::after{transform:translateY(2px) rotate(-135deg)}
130
+ :where(.sm-more) > :where(summary):hover{color:var(--fg)}
131
+ :where(.sm-more) > :where(:not(summary)){margin-top:var(--s-2,8px)}
132
+ :where(.sm-more[open]){padding-bottom:var(--s-3,12px)}
133
+ /* The popup spans its label row (.sm-label), so it never runs off a narrow screen. */
134
+ :where(.sm-label){position:relative}
135
+ :where(.sm-hint){display:inline-block;vertical-align:middle}
136
+ :where(:not(.sm-label)) > :where(.sm-hint){position:relative}
137
+ /* 44px to tap, with negative margins so the label row stays compact. */
138
+ :where(.sm-hint) > :where(summary){display:inline-grid;place-items:center;width:var(--touch-target,44px);height:var(--touch-target,44px);margin:-8px;min-height:0;padding:0;border:0;list-style:none;color:var(--fg-subtle);cursor:pointer}
139
+ :where(.sm-hint) > :where(summary)::-webkit-details-marker{display:none}
140
+ :where(.sm-hint) > :where(summary)::before{content:"i";display:grid;place-items:center;width:15px;height:15px;border:1.5px solid currentColor;border-radius:50%;font:600 10px/1 var(--font-body);font-style:normal}
141
+ :where(.sm-hint) > :where(summary):hover,:where(.sm-hint[open]) > :where(summary){color:var(--fg)}
142
+ :where(.sm-hint) > :where(:not(summary)){position:absolute;z-index:5;inset-inline-start:0;top:100%;width:max-content;max-width:min(280px,100%);margin:4px 0 0;padding:10px 12px;border:1px solid var(--border-tok);border-radius:var(--r-3,8px);background:var(--bg-elev-2);color:var(--fg);font-size:var(--fs-body-s,.875rem);line-height:1.45;box-shadow:var(--shadow-1)}
143
+ /* Outside a label row there is no safe anchor, so the explanation opens in the flow instead. */
144
+ :where(:not(.sm-label)) > :where(.sm-hint[open]){display:block}
145
+ :where(:not(.sm-label)) > :where(.sm-hint) > :where(:not(summary)){position:static;width:auto;max-width:60ch}
146
+
147
+ /* ---------- Glyphs: small icons drawn with currentColor ----------
148
+ <span class="sm-glyph" data-glyph="upload" aria-hidden="true"></span>
149
+ Pair with a visible word or the control's aria-label. */
150
+ :where(.sm-glyph){--g:var(--sm-glyph-size,20px);position:relative;display:inline-block;flex:none;width:var(--g);height:var(--g);color:inherit;vertical-align:middle}
151
+ :where(.sm-glyph)::before,:where(.sm-glyph)::after{content:"";position:absolute;box-sizing:border-box}
152
+ :where(.sm-glyph[data-glyph="play"])::before{inset:15% 15% 15% 25%;background:currentColor;clip-path:polygon(0 0,100% 50%,0 100%)}
153
+ :where(.sm-glyph[data-glyph="pause"])::before{inset:18% 25%;border:solid currentColor;border-width:0 calc(var(--g)*.14)}
154
+ :where(.sm-glyph[data-glyph="plus"])::before,:where(.sm-glyph[data-glyph="close"])::before{inset:15%;background:linear-gradient(currentColor,currentColor) center/100% 2px no-repeat,linear-gradient(currentColor,currentColor) center/2px 100% no-repeat}
155
+ :where(.sm-glyph[data-glyph="close"])::before{transform:rotate(45deg)}
156
+ :where(.sm-glyph[data-glyph="check"])::before{left:36%;top:12%;width:30%;height:58%;border:solid currentColor;border-width:0 2px 2px 0;transform:rotate(45deg)}
157
+ :where(.sm-glyph[data-glyph="chevron"])::before{left:30%;top:22%;width:40%;height:40%;border:solid currentColor;border-width:0 2px 2px 0;transform:rotate(45deg)}
158
+ :where(.sm-glyph[data-glyph="upload"])::before{left:10%;right:10%;bottom:10%;height:35%;border:2px solid currentColor;border-top:0;border-radius:0 0 3px 3px}
159
+ :where(.sm-glyph[data-glyph="upload"])::after{left:25%;right:25%;top:4%;height:62%;background:currentColor;clip-path:polygon(50% 0,100% 45%,60% 45%,60% 100%,40% 100%,40% 45%,0 45%)}
160
+ :where(.sm-glyph[data-glyph="image"])::before{inset:12%;border:2px solid currentColor;border-radius:3px}
161
+ :where(.sm-glyph[data-glyph="image"])::after{left:22%;right:22%;bottom:24%;height:34%;background:currentColor;clip-path:polygon(0 100%,35% 20%,60% 65%,75% 45%,100% 100%)}
162
+ :where(.sm-glyph[data-glyph="video"])::before{left:8%;right:28%;top:22%;bottom:22%;border:2px solid currentColor;border-radius:3px}
163
+ :where(.sm-glyph[data-glyph="video"])::after{right:8%;top:30%;bottom:30%;width:20%;background:currentColor;clip-path:polygon(0 35%,100% 0,100% 100%,0 65%)}
164
+ :where(.sm-glyph[data-glyph="audio"])::before{inset:15% 12%;background:linear-gradient(currentColor,currentColor) 0 60%/2px 40% no-repeat,linear-gradient(currentColor,currentColor) 33% 50%/2px 80% no-repeat,linear-gradient(currentColor,currentColor) 66% 50%/2px 100% no-repeat,linear-gradient(currentColor,currentColor) 100% 60%/2px 50% no-repeat}
165
+ :where(.sm-glyph[data-glyph="text"])::before{inset:20% 12%;background:linear-gradient(currentColor,currentColor) 0 0/100% 2px no-repeat,linear-gradient(currentColor,currentColor) 0 50%/100% 2px no-repeat,linear-gradient(currentColor,currentColor) 0 100%/60% 2px no-repeat}
166
+ :where(.sm-glyph[data-glyph="spark"])::before{inset:8%;background:currentColor;clip-path:polygon(50% 0,60% 40%,100% 50%,60% 60%,50% 100%,40% 60%,0 50%,40% 40%)}
167
+ :where(.sm-glyph[data-glyph="info"])::before{inset:10%;border:2px solid currentColor;border-radius:50%}
168
+ :where(.sm-glyph[data-glyph="info"])::after{left:50%;top:28%;width:2px;height:46%;margin-left:-1px;background:linear-gradient(currentColor,currentColor) 0 0/2px 2px no-repeat,linear-gradient(currentColor,currentColor) 0 100%/2px 60% no-repeat}
169
+ :where(.sm-glyph[data-glyph="warning"])::before{inset:8% 4% 10%;background:currentColor;clip-path:polygon(50% 0,100% 100%,0 100%)}
170
+ :where(.sm-glyph[data-glyph="warning"])::after{left:50%;top:38%;width:2px;height:38%;margin-left:-1px;background:linear-gradient(var(--bg),var(--bg)) 0 0/2px 60% no-repeat,linear-gradient(var(--bg),var(--bg)) 0 100%/2px 2px no-repeat}
171
+ :where(.sm-glyph[data-glyph="spinner"])::before{inset:12%;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:sm-spin .8s linear infinite}
172
+
173
+ /* The hidden attribute always wins over kit display rules. */
174
+ :where([class*="sm-"][hidden]){display:none!important}
175
+
176
+ /* Visually hidden but read aloud, for icon-only controls that need more than aria-label. */
177
+ :where(.sm-sr){position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
178
+
179
+ @keyframes sm-spin{to{transform:rotate(360deg)}}
180
+ @media (prefers-reduced-motion:reduce){
181
+ :where(.sm-status[data-state="loading"])::before,:where(.sm-glyph[data-glyph="spinner"])::before{animation:none;border-right-color:currentColor;opacity:.6}
182
+ :where(.sm-more) > :where(summary)::after{transition:none}
183
+ }