stillmade-sdk 0.1.49 → 0.1.51
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/AGENTS.md +6 -2
- package/AGENT_CONTROL.md +15 -0
- package/FRAME_VIEWS.md +30 -0
- package/INTERFACE_DESIGN.md +84 -7
- package/MODULES.md +2 -2
- package/PASTE_TO_LLM.md +26 -19
- package/SDK.md +180 -45
- package/UI_AESTHETIC.md +15 -0
- package/blocks/generate/package.js +1 -1
- package/blocks/generate/versions/1.1.0/package.js +471 -0
- package/blocks/generate/versions/1.2.0/package.js +555 -0
- package/blocks/native/sm.board-editor/versions/3.0.0/package.js +50 -17
- package/blocks/native/sm.board-editor/versions/3.1.0/package.js +1037 -0
- package/blocks/native/sm.editor/versions/3.0.0/package.js +128 -83
- package/blocks/native/sm.editor/versions/3.1.0/package.js +1299 -0
- package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +45 -12
- package/blocks/native/sm.panel-strip/versions/3.1.0/package.js +479 -0
- package/blocks/native/sm.voiceover/versions/3.0.0/package.js +9 -3
- package/docs/build/block-builder-contract.md +6 -2
- package/docs/build/frame-views.md +30 -0
- package/docs/build/interface-design.md +84 -7
- package/docs/build/modules.md +2 -2
- package/docs/reference/authoring-guide.md +4 -4
- package/docs/reference/errors.md +1 -1
- package/docs/reference/execution.md +18 -4
- package/docs/reference/first-party-blocks.md +4 -4
- package/docs/reference/parity.md +1 -1
- package/docs/reference/ports.md +1 -1
- package/docs/reference/view-bridge.md +3 -0
- package/docs/tools/llm.md +26 -19
- package/package.json +1 -1
- package/packages/block-cli/dev-host/assets/host.js +161 -33
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-platform/block-readiness.js +16 -3
- package/packages/block-platform/canvas.js +4 -1
- package/packages/block-platform/catalog.js +32 -5
- package/packages/block-platform/flow-inspection.js +30 -2
- package/packages/block-platform/listings.js +16 -2
- package/packages/block-platform/steps.js +4 -1
- package/packages/block-sdk/admission.js +2 -2
- package/packages/block-sdk/agent-inputs.js +4 -2
- package/packages/block-sdk/appearance.js +19 -4
- package/packages/block-sdk/builder-contract.js +7 -4
- package/packages/block-sdk/first-party-identities.js +9 -1
- package/packages/block-sdk/first-party-packages.js +11 -1
- package/packages/block-sdk/generation-catalog.js +55 -5
- package/packages/block-sdk/generation-models.js +60 -0
- package/packages/block-sdk/index.d.ts +18 -4
- package/packages/block-sdk/index.js +2 -1
- package/packages/block-sdk/input-source-policy.js +2 -1
- package/packages/block-sdk/module-hosted.js +13 -5
- package/packages/block-sdk/module-worker.js +1 -1
- package/packages/block-sdk/port-types.js +6 -2
- package/packages/block-sdk/semantic-contracts.d.ts +1 -1
- package/packages/block-sdk/semantic-relationships.js +3 -0
- package/packages/block-sdk/semantic-schema.js +4 -1
- package/packages/block-sdk/shared-controls.js +4 -1
- package/packages/block-sdk/staged-releases.js +2 -1
- package/packages/block-sdk/testing.js +3 -0
- package/packages/block-sdk/view-bridge.js +3 -0
- package/packages/block-sdk/view-design.js +56 -12
- package/packages/block-sdk/view-renderer.js +19 -6
- package/packages/block-sdk/view.js +3 -2
- package/packages/design-system/block-kit.css +12 -3
|
@@ -15,9 +15,9 @@ export interface SdkCompatibility { status:'supported'|'unsupported'|'invalid';
|
|
|
15
15
|
export function sdkCompatibility(declared:unknown,runtimeVersion?:string):SdkCompatibility;
|
|
16
16
|
export type Port = { type: string; imageMode?: 'pixels'|'reference'; role?: string; context?: keyof typeof CONTEXT_FIELDS; primary?: boolean; required?: boolean; default?: unknown; min?: number; max?: number; description?: string } & Partial<import('./semantic-contracts.js').SemanticContract>;
|
|
17
17
|
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; action?: { label: string; running?: string }; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record<string, Port>; outputs: Record<string, Port>; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record<string,unknown>;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; desktopTools?:DesktopToolDeclaration[]; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
|
|
18
|
-
export interface BlockView { html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'; }
|
|
18
|
+
export interface BlockView { html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'|'none'; }
|
|
19
19
|
/** A frame view: any interface code (frameworks, SVG, canvas, drag) with assets named by SHA-256. */
|
|
20
|
-
export interface FrameView { runtime: 'frame'; html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'; files?: {schemaVersion: 1; files: Record<string, {sha256: string; bytes: number; mediaType: string}>}; }
|
|
20
|
+
export interface FrameView { runtime: 'frame'; html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'|'none'; files?: {schemaVersion: 1; files: Record<string, {sha256: string; bytes: number; mediaType: string}>}; }
|
|
21
21
|
export interface BlockPackage { portable?: PortableMetadata; manifest: Manifest; view?: BlockView|FrameView; code?: string; module?: ModuleDescriptor; comfyui?: ComfyWorkflow; capability?: ConnectedCapability|TextGenerationCapability|SpeechCapability|TranscriptionCapability|ImageGenerationCapability|VideoGenerationCapability|ImageDescriptionCapability|TimelineProposalCapability; recipe?: { schemaVersion: 1; steps: { id: string; op: string; args: Record<string, unknown> }[]; outputs: Record<string, unknown> }; tests: { name: string; input: Record<string, unknown>; expected?: Record<string, unknown>; state?: BlockState; expectedState?: BlockState; expectations?: Record<string, ConnectedResultExpectation|ComfyImageExpectation|CapabilityTextExpectation|CapabilityAudioExpectation|CapabilityTranscriptExpectation|CapabilityImageExpectation|CapabilityVideoExpectation|CapabilityTimelineExpectation> }[]; }
|
|
22
22
|
/** Named sandbox action with fixed declared input settings and produced ports. */
|
|
23
23
|
export interface BlockOperation {id:string;label:string;description:string;inputs:Record<string,string|number|boolean>;outputs:string[]}
|
|
@@ -178,7 +178,7 @@ export interface BlockMediaPreview { kind: 'video'|'audio'; url: string; mimeTyp
|
|
|
178
178
|
export interface BlockViewBridge { useOutput(outputs: Record<string, BlockMediaReference|BlockMediaReference[]>): Promise<Record<string, unknown>>; previewMedia(value: BlockMediaReference): Promise<BlockMediaPreview>; releaseMedia(url: string): void; render(targetId: string, tree: BlockViewNode[]): void; onAction(callback: (event: BlockViewAction) => void|Promise<void>): () => void; desktop(operation: DesktopPermission|DesktopToolOperation|import('./block-storage.js').BlockStorageOperation|"record.start"|"record.stop"|"record.cancel"|"session.close"|"live.start"|"live.stop"|"live.status"|"folder.pick"|"folder.shared"|"folder.forget"|"folder.list", args?: Record<string,unknown>): Promise<Record<string,unknown>>; readonly input: Record<string, unknown>; onInput(callback: (input: Record<string, unknown>) => void): () => void; run(input: Record<string, unknown>): Promise<Record<string, unknown>>; previewImage(value: BlockImageReference|BlockImagePixels, options?: BlockImagePreviewOptions): Promise<BlockImagePreview>; }
|
|
179
179
|
|
|
180
180
|
/** Validate hosted UI colors independently of the preserved original stylesheet. */
|
|
181
|
-
export function validateViewAppearance(view?: BlockView, options?: {appearance?:'host'|'original'}): true;
|
|
181
|
+
export function validateViewAppearance(view?: BlockView, options?: {appearance?:'host'|'original'|'none'}): true;
|
|
182
182
|
export const APPEARANCE_TOKENS: readonly string[];
|
|
183
183
|
|
|
184
184
|
/** Inert API prompt for reviewed core image nodes. Endpoints and credentials belong to the host. */
|
|
@@ -275,6 +275,7 @@ export interface VideoGenerationModel {label:string;aspectRatios:readonly string
|
|
|
275
275
|
export interface ImageSettings {model:string;aspectRatio:string;resolution:string;quality:string}
|
|
276
276
|
export interface VideoSettings {model:string;duration:number;resolution:string;aspectRatio:string;generateAudio:boolean}
|
|
277
277
|
/** Every image model a Block can use; the same catalog as the app's own Blocks. */
|
|
278
|
+
/** The image and video models this SDK version knew when it was published. A Block's screen lists the models StillMade runs now with `StillMade.models(operation)`; never copy this list into a Block. */
|
|
278
279
|
export const IMAGE_GENERATION_MODELS: Readonly<Record<string,ImageGenerationModel>>;
|
|
279
280
|
/** Every video model a Block can use; the same catalog as the app's own Blocks. */
|
|
280
281
|
export const VIDEO_GENERATION_MODELS: Readonly<Record<string,VideoGenerationModel>>;
|
|
@@ -288,6 +289,19 @@ export function imageSettings(value?:Partial<ImageSettings>,options?:{strict?:bo
|
|
|
288
289
|
/** Fill and check video settings, including Veo's 1080p-is-8-seconds rule. */
|
|
289
290
|
/** With no model named, the default is the cheapest model that makes the asked aspect ratio and takes the frames given. */
|
|
290
291
|
export function videoSettings(value?:Partial<VideoSettings>,options?:{strict?:boolean;firstFrame?:boolean;lastFrame?:boolean}):VideoSettings;
|
|
292
|
+
/** Model ids StillMade replaced, each pointing at its replacement: a Block or Step that names one runs on the replacement. */
|
|
293
|
+
export const RETIRED_IMAGE_MODELS: Readonly<Record<string,string>>;
|
|
294
|
+
export const RETIRED_VIDEO_MODELS: Readonly<Record<string,string>>;
|
|
295
|
+
/** The image model that runs for this id: itself, or the model that replaced it. */
|
|
296
|
+
export function currentImageModel(id:string):string;
|
|
297
|
+
/** The video model that runs for this id: itself, or the model that replaced it. */
|
|
298
|
+
export function currentVideoModel(id:string):string;
|
|
299
|
+
/** Whether a catalog model takes every setting asked for (a setting left out or 'default' always fits). */
|
|
300
|
+
export function modelTakes(kind:'image'|'video',model:string,settings?:Record<string,unknown>):boolean;
|
|
301
|
+
/** The hosted operations a Block may list models for: those it declares, and what its media actions make. */
|
|
302
|
+
export function modelOperationsFor(manifest:unknown):string[];
|
|
303
|
+
/** StillMade.models' answer from this SDK's own catalog, without prices (the SDK preview and tests). */
|
|
304
|
+
export function catalogModelList(operation:string):{operation:string;default:string|null;models:Array<{id:string;label:string;credits:number|null;available:boolean;settings:Array<{key:string;label:string;values:Array<string|number|boolean>;default:string|number|boolean}>;[key:string]:unknown}>};
|
|
291
305
|
export function validateSeed(seed:number):number;
|
|
292
306
|
export const MEDIA_GENERATE_LIMITS: Readonly<{items:50;prompt:4000}>;
|
|
293
307
|
export const MEDIA_GENERATE_OPERATIONS: readonly ['image','video','speech','music','sfx'];
|
|
@@ -652,7 +666,7 @@ export function moduleRunClock(now?: () => number): Readonly<{progress(): void;
|
|
|
652
666
|
export interface ModuleHost {
|
|
653
667
|
input: Record<string, unknown>;
|
|
654
668
|
signal: AbortSignal;
|
|
655
|
-
progress(value: number, label?: string): void;
|
|
669
|
+
progress(value: number, label?: string, items?: {done: number; total: number}): void;
|
|
656
670
|
log(...values: unknown[]): void;
|
|
657
671
|
/** A copy of one of the module's own files, such as a .wasm build. */
|
|
658
672
|
asset(path: string): ArrayBuffer;
|
|
@@ -39,7 +39,8 @@ export {isTimelineEdit} from './timeline-edits.js';
|
|
|
39
39
|
|
|
40
40
|
export {isWorkspaceEdit,WORKSPACE_EDIT_FIELDS} from './workspace-edits.js';
|
|
41
41
|
export {portableMediaAction,PORTABLE_MEDIA_ACTION_KIND,mediaGenerateRequest,MEDIA_GENERATE_LIMITS,MEDIA_GENERATE_OPERATIONS} from './portable-media-actions.js';
|
|
42
|
-
export {IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS,DEFAULT_IMAGE_MODEL,DEFAULT_VIDEO_MODEL,DEFAULT_SETTING,GENERATION_LIMITS,AUDIO_GENERATION,imageSettings,videoSettings,audioSettings,validateSeed} from './generation-catalog.js';
|
|
42
|
+
export {IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS,DEFAULT_IMAGE_MODEL,DEFAULT_VIDEO_MODEL,DEFAULT_SETTING,GENERATION_LIMITS,AUDIO_GENERATION,imageSettings,videoSettings,audioSettings,validateSeed,RETIRED_IMAGE_MODELS,RETIRED_VIDEO_MODELS,currentImageModel,currentVideoModel,modelTakes} from './generation-catalog.js';
|
|
43
|
+
export {modelOperationsFor,catalogModelList} from './generation-models.js';
|
|
43
44
|
export {JSON_SCHEMA_LIMITS,validateJsonSchema,jsonSchemaMismatch,jsonSchemaInstruction,parseJsonReply} from './json-schema.js';
|
|
44
45
|
export {MODULE_RUNTIME_VERSION,MODULE_LIMITS,MODULE_MEDIA_TYPES,MODULE_HOST_API,validateModuleDescriptor,moduleFileDigest,describeModuleFiles,moduleMediaType,verifyModuleFile,moduleDeadline,moduleRunClock} from './module-runtime.js';
|
|
45
46
|
export {MEDIA_TRANSFORM_LIMITS,MEDIA_TRANSFORM_OPERATIONS,mediaTransformRequest,mediaTransformSources,silentRanges,keepRanges,integratedLoudness,loudnessGain,loudnessMeter,gainFor,wavWriter,encodeWav,decodeWav} from './media-transform.js';
|
|
@@ -16,6 +16,7 @@ export function assertSuppliedInputSources(manifest,config,supplied,{connected=[
|
|
|
16
16
|
if(port.context || connected.includes(name) && name!==selectedPrimary)continue;
|
|
17
17
|
const sameSetting=Object.hasOwn(config,name) && Object.hasOwn(supplied,name) && stableStringify(config[name])===stableStringify(supplied[name]);
|
|
18
18
|
if(name===selectedPrimary || Object.hasOwn(supplied,name) && !sameSetting)assertInputSource(port,'user',name);
|
|
19
|
-
|
|
19
|
+
// A value the Block's own screen passed in its last run is kept with the Step like any run input.
|
|
20
|
+
else if(Object.hasOwn(config,name))assert(inputSourceAllowed(port,'user') || inputSourceAllowed(port,'default') || inputSourceAllowed(port,'view'),'INPUT_SOURCE',`${name} requires a connected or resolved source, not a saved answer.`);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
// operation in `permissions.capabilities`.
|
|
6
6
|
import {assert,plain,stableStringify} from './contracts.js';
|
|
7
7
|
import {defaultCapabilityExpectations,MEDIA_ANALYZE_LIMITS} from './capabilities.js';
|
|
8
|
-
import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS} from './generation-catalog.js';
|
|
8
|
+
import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS,runnableSettings} from './generation-catalog.js';
|
|
9
|
+
|
|
10
|
+
// November 8, 2026 (owner: a setting never breaks a run): a module's image and video settings run as StillMade can run
|
|
11
|
+
// them (runnableSettings): Image Panels 3.0.0 sent quality and seed with its clips and a quality to models without one,
|
|
12
|
+
// which the strict check refused. A seed in settings is the call's seed.
|
|
13
|
+
const validSeed=value=>Number.isSafeInteger(value)&&value>=0&&value<=GENERATION_LIMITS.seed;
|
|
14
|
+
const callSeed=request=>validSeed(request.seed)?request.seed:validSeed(request.settings?.seed)?request.settings.seed:undefined;
|
|
9
15
|
|
|
10
16
|
const text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max;
|
|
11
17
|
const media=(value,kind)=>plain(value)&&value.kind===kind&&['assetId','versionId','url'].every(key=>typeof value[key]==='string'&&value[key].trim());
|
|
@@ -26,9 +32,10 @@ const SHAPES={
|
|
|
26
32
|
const references=request.references;
|
|
27
33
|
if(references!==undefined&&(!Array.isArray(references)||!references.length||references.length>GENERATION_LIMITS.references||!references.every(value=>media(value,'image'))))fail(`image.generate references are 1 to ${GENERATION_LIMITS.references} saved images`);
|
|
28
34
|
// A model named in settings must take that many photos (GPT-Image 1.5, Kling Image V3 and Seedream 4.5 take none).
|
|
29
|
-
const
|
|
35
|
+
const settings=runnableSettings('image',request.settings),seed=callSeed(request);
|
|
36
|
+
const model=IMAGE_GENERATION_MODELS[settings?.model];
|
|
30
37
|
if(references&&model&&references.length>model.references)fail(model.references?`${model.label} takes at most ${model.references} reference photos`:`${model.label} takes no reference photos: choose a model that does, or leave the model to the person`);
|
|
31
|
-
return {inputs:{prompt:port('text','Image prompt'),...(references?{references:port('image[]','Reference images')}:{})},outputs:{result:port('image','Generated image')},capability:{prompt:{$input:'prompt'},output:'result',...(references?{references:{$input:'references'}}:{}),...(
|
|
38
|
+
return {inputs:{prompt:port('text','Image prompt'),...(references?{references:port('image[]','Reference images')}:{})},outputs:{result:port('image','Generated image')},capability:{prompt:{$input:'prompt'},output:'result',...(references?{references:{$input:'references'}}:{}),...(settings?{settings}:{}),...(request.negativePrompt?{negativePrompt:request.negativePrompt}:{}),...(seed!==undefined?{seed}:{})},input:{prompt:request.prompt,...(references?{references}:{})}};
|
|
32
39
|
},
|
|
33
40
|
'video.generate':request=>{
|
|
34
41
|
if(!text(request.prompt,32000))fail('video.generate needs a prompt');
|
|
@@ -41,11 +48,12 @@ const SHAPES={
|
|
|
41
48
|
if(negativePrompt!==undefined&&!(typeof negativePrompt==='string'&&negativePrompt.length<=GENERATION_LIMITS.negativePrompt))fail(`video.generate negativePrompt is text of at most ${GENERATION_LIMITS.negativePrompt.toLocaleString('en-US')} characters`);
|
|
42
49
|
// A model named in settings must take those frames (every model takes a first frame; Kling 3.0 Turbo and Wan 2.6 Flash
|
|
43
50
|
// take no last frame; Wan 2.6 Flash and Wan 2.7 need a first frame).
|
|
44
|
-
const
|
|
51
|
+
const settings=runnableSettings('video',request.settings,{firstFrame:firstFrame!==undefined,lastFrame:lastFrame!==undefined}),seed=callSeed(request);
|
|
52
|
+
const model=VIDEO_GENERATION_MODELS[settings?.model];
|
|
45
53
|
if(model&&firstFrame&&!model.firstFrame)fail(`${model.label} takes no first frame: choose a model that does, or leave the model to the person`);
|
|
46
54
|
if(model&&lastFrame&&!model.lastFrame)fail(`${model.label} takes no last frame: choose a model that does, or leave the model to the person`);
|
|
47
55
|
if(model?.requiresFirstFrame&&!firstFrame)fail(`${model.label} needs a firstFrame`);
|
|
48
|
-
return {inputs:{prompt:port('text','Video prompt'),...(firstFrame?{firstFrame:port('image','First frame')}:{}),...(lastFrame?{lastFrame:port('image','Last frame')}:{})},outputs:{result:port('video','Generated video')},capability:{prompt:{$input:'prompt'},output:'result',...(firstFrame?{firstFrame:{$input:'firstFrame'}}:{}),...(lastFrame?{lastFrame:{$input:'lastFrame'}}:{}),...(
|
|
56
|
+
return {inputs:{prompt:port('text','Video prompt'),...(firstFrame?{firstFrame:port('image','First frame')}:{}),...(lastFrame?{lastFrame:port('image','Last frame')}:{})},outputs:{result:port('video','Generated video')},capability:{prompt:{$input:'prompt'},output:'result',...(firstFrame?{firstFrame:{$input:'firstFrame'}}:{}),...(lastFrame?{lastFrame:{$input:'lastFrame'}}:{}),...(settings?{settings}:{}),...(negativePrompt?{negativePrompt}:{}),...(seed!==undefined?{seed}:{})},input:{prompt:request.prompt,...(firstFrame?{firstFrame}:{}),...(lastFrame?{lastFrame}:{})}};
|
|
49
57
|
},
|
|
50
58
|
'audio.speech':request=>{
|
|
51
59
|
if(!text(request.text,3800))fail('audio.speech needs text of up to 3,800 characters');
|
|
@@ -51,7 +51,7 @@ export const MODULE_WORKER_SOURCE = String.raw`(function(scope){
|
|
|
51
51
|
function call(api,args,transfer){return new Promise(function(resolve,reject){var id=++sequence;pending.set(id,{resolve:resolve,reject:reject});post({type:'call',id:id,api:api,args:args},transfer);});}
|
|
52
52
|
var host=Object.freeze({
|
|
53
53
|
signal:controller.signal,
|
|
54
|
-
progress:function(value,label){var number=Number(value);post({type:'progress',value:Number.isFinite(number)?Math.max(0,Math.min(1,number)):null,label:String(label==null?'':label).slice(0,200)});},
|
|
54
|
+
progress:function(value,label,items){var number=Number(value),done=items&&Number(items.done),total=items&&Number(items.total),counted=Number.isSafeInteger(done)&&Number.isSafeInteger(total)&&total>0&&done>=0&&done<=total;post(Object.assign({type:'progress',value:Number.isFinite(number)?Math.max(0,Math.min(1,number)):null,label:String(label==null?'':label).slice(0,200)},counted?{done:done,total:total}:{}));},
|
|
55
55
|
log:function(){post({type:'log',message:Array.prototype.map.call(arguments,text).join(' ').slice(0,2000)});},
|
|
56
56
|
// A copy of one of the module's own files (for example a .wasm build).
|
|
57
57
|
asset:function(path){var bytes=files&&Object.prototype.hasOwnProperty.call(files,path)?files[path]:null;if(!bytes)throw new Error(path+' is not a file of this module');return bytes.slice(0);},
|
|
@@ -15,7 +15,7 @@ export const PORT_VALUE_SHAPES=Object.freeze({
|
|
|
15
15
|
text:'a string',number:'a number',integer:'a whole number',boolean:'true or false',json:'any JSON value',object:'a plain JSON object',
|
|
16
16
|
url:'an absolute http or https address string',date:"an ISO 8601 date string ('2026-10-09' or '2026-10-09T14:30')",color:"a hex color string ('#ff8800' or '#ff8800cc')",
|
|
17
17
|
image:`{kind: 'image'; ${MEDIA_FIELDS}; width?: number; height?: number}`,
|
|
18
|
-
video:`{kind: 'video'; ${MEDIA_FIELDS}; width?: number; height?: number; duration?: number}`,
|
|
18
|
+
video:`{kind: 'video'; ${MEDIA_FIELDS}; width?: number; height?: number; duration?: number; parts?: {shots: {start: number; end: number; from?: number; source: {kind: string; assetId: string; versionId: string; url?: string}}[]; lines: {start: number; end: number; text: string}[]}}`,
|
|
19
19
|
audio:`{kind: 'audio'; ${MEDIA_FIELDS}; duration?: number; sampleRate?: number; channels?: number}`,
|
|
20
20
|
asset:`{kind: 'image' | 'video' | 'audio' | 'asset'; ${MEDIA_FIELDS}}`,
|
|
21
21
|
file:`{kind: 'file'; assetId: string; versionId: string; mimeType: ${quoted(FILE_MEDIA_TYPES)}; url?: string; name?: string; bytes?: number}`,
|
|
@@ -79,6 +79,10 @@ type StillMadeTable = ${PORT_VALUE_SHAPES.table};
|
|
|
79
79
|
type StillMadeTranscript = ${PORT_VALUE_SHAPES.transcript};
|
|
80
80
|
type StillMadeScript = ${PORT_VALUE_SHAPES.script};
|
|
81
81
|
type StillMadeDocumentValue = ${STRUCTURED_VALUE_SHAPE};
|
|
82
|
+
/** StillMade.models(operation): the models StillMade runs now, each setting as data. */
|
|
83
|
+
type StillMadeModelList = {operation: string; default: string | null; models: Array<{id: string; label: string; credits: number | null; fromCredits?: number; available: boolean; reason?: string; settings: Array<{key: string; label: string; values: Array<string | number | boolean>; default: string | number | boolean}>; references?: number; firstFrame?: boolean; lastFrame?: boolean; needsFirstFrame?: boolean; sound?: boolean; voices?: Array<{id: string; label: string}>}>};
|
|
84
|
+
/** StillMade.nextRun(plan): what the screen's next run makes, which StillMade prices on its run button. */
|
|
85
|
+
type StillMadeNextRun = {items?: Array<{key?: string; operation: 'image' | 'video' | 'speech' | 'music' | 'sfx'; prompt?: string; settings?: Record<string, string | number | boolean>; references?: StillMadeImage[]; firstFrame?: StillMadeImage; lastFrame?: StillMadeImage; negativePrompt?: string; seed?: number}>; hosted?: Array<string | {operation: string; count?: number}>};
|
|
82
86
|
|
|
83
87
|
/** ${manifest.name}: input values. */
|
|
84
88
|
export interface BlockInputs {
|
|
@@ -98,7 +102,7 @@ ${bridge}
|
|
|
98
102
|
export interface StillMadeModule {
|
|
99
103
|
readonly input: BlockInputs;
|
|
100
104
|
readonly signal: AbortSignal;
|
|
101
|
-
progress(value: number, label?: string): void;
|
|
105
|
+
progress(value: number, label?: string, items?: {done: number; total: number}): void;
|
|
102
106
|
log(...values: unknown[]): void;
|
|
103
107
|
asset(path: string): ArrayBuffer;
|
|
104
108
|
files: {read(ref: StillMadeMedia | StillMadeImage | StillMadeVideo | StillMadeAudio | StillMadeFile): Promise<ArrayBuffer>; write(data: ArrayBuffer | ArrayBufferView | Blob | string, options: {mediaType: string; name?: string}): Promise<StillMadeMedia>};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type SemanticSchema = string | {[field:string]:SemanticSchema} | readonly [SemanticSchema];
|
|
2
|
-
export type RequirementSource = 'upstream'|'project'|'adapter'|'chat'|'default'|'user'|'external';
|
|
2
|
+
export type RequirementSource = 'upstream'|'project'|'adapter'|'chat'|'default'|'user'|'external'|'view';
|
|
3
3
|
export interface SemanticContract {
|
|
4
4
|
key:string;
|
|
5
5
|
type:string;
|
|
@@ -28,6 +28,9 @@ export function standardSemanticIsA(actual,expected) {
|
|
|
28
28
|
// A connected task's own input fields (email, phone, message…) take any value
|
|
29
29
|
// of the right type that the user wires to them, such as a lead's email.
|
|
30
30
|
if(/^external_input_[a-f0-9]{16}_\d+$/.test(expected))return true;
|
|
31
|
+
// Media to edit (November 7, 2026, the Editor's media): any picture, clip or sound a Step hands on, whatever it means;
|
|
32
|
+
// the port's type still decides which files fit.
|
|
33
|
+
if(expected==='editable_media'&&typeof actual==='string')return true;
|
|
31
34
|
if(typeof actual!=='string' || typeof expected!=='string')return false;
|
|
32
35
|
// Every connected-app task names its own result meaning; all are external data.
|
|
33
36
|
if(expected==='external_data' && /^external_result_[a-f0-9]{16}$/.test(actual))return true;
|
|
@@ -73,7 +73,10 @@ export function schemaAssignable(source,target) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export const SEMANTIC_NAME=/^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
76
|
-
|
|
76
|
+
// 'view' (November 7, 2026): only the Block's own screen fills it, in the runs it starts (a frame view's picture to edit,
|
|
77
|
+
// the file it saves). No wire, project field, Chat or person's saved answer supplies it, and nothing offers it as a
|
|
78
|
+
// place for the person's own files.
|
|
79
|
+
export const REQUIREMENT_SOURCES=Object.freeze(['upstream','project','adapter','chat','default','user','external','view']);
|
|
77
80
|
export const SEMANTIC_PORT_FIELDS=Object.freeze(['key','semantic','accepts','schema','sources','sensitivity','freshnessPolicy','cardinality','batchable']);
|
|
78
81
|
|
|
79
82
|
export function semanticPortErrors(port,{key,direction='inputs'}={}) {
|
|
@@ -226,6 +226,9 @@ export function installSharedControls(document,api,onError,textControls=null,loc
|
|
|
226
226
|
// state each time hid a view that overwrote other people's edits from the two-participant check.
|
|
227
227
|
const previous=inputDefaults;inputDefaults={};
|
|
228
228
|
if(input&&typeof input==='object'&&!Array.isArray(input))for(const [key,value] of Object.entries(input))if(['string','number','boolean'].includes(typeof value))inputDefaults[key]=value;
|
|
229
|
-
|
|
229
|
+
// A shared text control has no setInputDefault: its plain default is the input's value. One control that cannot take
|
|
230
|
+
// its default never stops the rest (November 8, 2026: every control bound after the first text field missed its Step
|
|
231
|
+
// input, found building Generate 1.2.0).
|
|
232
|
+
for(const [key,binding] of bindings)if(Object.hasOwn(inputDefaults,key)&&!(Object.hasOwn(previous,key)&&previous[key]===inputDefaults[key]))try{(binding.setInputDefault||binding.setDefault)?.call(binding,inputDefaults[key]);}catch{}
|
|
230
233
|
},dispose(){closed=true;observer?.disconnect();if(scanTimer)clearTimeout(scanTimer);for(const entry of deferred.values())clearTimeout(entry.timer);deferred.clear();for(const binding of [...bindings.values()])binding.dispose();}};
|
|
231
234
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Releases that resolve for exact pins but are not offered yet: never discovered, picked as a Block's newest release or
|
|
2
2
|
// proposed as an update (November 6, 2026: Voiceover 3.0.0 waits for the built-in Types to switch to the replaced
|
|
3
3
|
// screens together; docs/BUILTIN_BLOCK_REPLACEMENTS_PLAN.md). Remove an entry to release it.
|
|
4
|
-
|
|
4
|
+
// November 7, 2026 (owner: "yes switch them"): Voiceover, Image Panels, the board editor and the Editor 3.0.0 are released.
|
|
5
|
+
export const STAGED_FIRST_PARTY_RELEASES = Object.freeze(new Set([]));
|
|
5
6
|
export const isStagedRelease = manifest => STAGED_FIRST_PARTY_RELEASES.has(`${manifest?.id}@${manifest?.version}`);
|
|
@@ -8,6 +8,7 @@ import { validateRecipe } from './recipe.js';
|
|
|
8
8
|
import { validateJavaScript } from './javascript.js';
|
|
9
9
|
import { runPackageAsync } from './runtime.js';
|
|
10
10
|
import {validateView} from './view.js';
|
|
11
|
+
import {interfaceMode} from './view-design.js';
|
|
11
12
|
import {validateComfyWorkflow,validateComfyExpectations,testComfyOutputs} from './comfyui.js';
|
|
12
13
|
import {CAPABILITY_LIMITS,validateCapability,prepareCapabilityInvocation,validateCapabilityExpectations,testCapabilityOutputs} from './capabilities.js';
|
|
13
14
|
import {validateNativeApplicationPackage,NATIVE_APPLICATION_RUNTIME_REQUIREMENT} from './native-application-package.js';
|
|
@@ -83,6 +84,8 @@ export function validatePackage(pkg) {
|
|
|
83
84
|
else if (pkg.manifest.runtime === 'javascript') { assert(!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module')&&!Object.hasOwn(pkg,'moduleSource'),'PACKAGE','Only matching runtimes can contain ComfyUI workflows, host requests or module files'); validateJavaScript(pkg); }
|
|
84
85
|
else { assert(!Object.hasOwn(pkg, 'code')&&!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module'), 'PACKAGE', 'Recipe packages cannot contain code, ComfyUI workflows or host requests'); validateRecipe(pkg.recipe); }
|
|
85
86
|
if(pkg.view !== undefined)validateView(pkg.view);
|
|
87
|
+
// November 9, 2026 (owner): an app may drop StillMade's style; a job keeps it, so a job's page stays StillMade's.
|
|
88
|
+
assert(pkg.view?.appearance!=='none'||interfaceMode(pkg.manifest)==='app','VIEW','Only an app (manifest.kind "workspace") can set view appearance "none"; a job keeps StillMade\'s style. Use "host", or "original" for colors of its own.');
|
|
86
89
|
assert(Array.isArray(pkg.tests) && pkg.tests.length > 0 && pkg.tests.length <= 50, 'TESTS_REQUIRED', 'Include 1–50 fixture tests');
|
|
87
90
|
if(requiresDesktopExecution(pkg))for(const fixture of pkg.tests){
|
|
88
91
|
assert(fixture&&typeof fixture.name==='string'&&fixture.name.trim().length>0,'TEST','Desktop fixtures need a name and typed input/output contracts.');
|
|
@@ -15,6 +15,8 @@ export const VIEW_BRIDGE=Object.freeze([
|
|
|
15
15
|
{name:'removeFile',group:'Inputs and runs',signature:'removeFile(port: string, index: number): Promise<{files: number}>',summary:'Removes the file at `index` (from 0) of a declared list input (`image[]`, `video[]`, `audio[]`, `asset[]` or `file[]`), as the remove button on the Step\'s own tile does, and resolves to how many files the list now holds. Call it from a click or key press in the view; the new list arrives through `onInput`. A `<button data-stillmade-remove="port" data-index="2">` does this with no code. Refused for an undeclared, non-file or single input, an input an earlier Step fills, a viewer, Chat, a file that changed since the view was given it, and without a gesture.',where:`${saved} and the SDK preview`},
|
|
16
16
|
{name:'moveFile',group:'Inputs and runs',signature:'moveFile(port: string, from: number, to: number): Promise<{files: number}>',summary:'Moves the file at `from` to the place `to` (both from 0) in a declared list input, as the Step\'s own move earlier and move later buttons do, so the person sets the order (a tour, a slideshow) on the Block\'s own screen. Same gesture and refusals as `removeFile`. A `<button data-stillmade-move="port" data-index="2" data-to="1">` does this with no code; a `data-to` past either end does nothing.',where:`${saved} and the SDK preview`},
|
|
17
17
|
{name:'run',group:'Inputs and runs',signature:'run(values: In): Promise<Out>',summary:'Checks the named input values, runs the Block (its recipe, JavaScript, module or hosted operation, after any approval the host asks for) and resolves to the checked outputs. On a project Step and in the SDK preview, inputs it leaves out run with the Step\'s current values (its photos, an earlier Step\'s result), so a view passes only what it sets; what it passes is kept as the Step\'s input. One run at a time, 30 a minute.',where:all},
|
|
18
|
+
{name:'models',group:'Inputs and runs',signature:'models(operation: string): Promise<StillMadeModelList>',summary:'The models StillMade runs right now for a hosted operation the Block declares (`image.generate`, `video.generate`, `audio.speech`, `text.generate` and the rest; a Block with the `media.generate` action may ask for image, video, speech, music and sound effects): each model\'s `id`, label, credits per call, whether it is available, its settings as data (`{key, label, values, default}`: shape, size, quality, length, sound) and what it takes (reference photos, first and last frames), with `default`, the model a run uses when none is named. Never keep a list of models in a Block: draw the picker from this when the screen opens, so models StillMade adds or retires later reach it with no new version, and pass the chosen `id` as `settings.model`. Leave the model out when the person did not pick one. The SDK preview answers from the SDK\'s own catalog, without prices.',where:all},
|
|
19
|
+
{name:'nextRun',group:'Inputs and runs',signature:'nextRun(plan: StillMadeNextRun | null): void',summary:'Tells StillMade what the screen\'s next run makes, so StillMade writes its price on the screen\'s run button (`data-stillmade-action="run"`), as its own run buttons read: "Generate image · 4 credits". `items` are the `media.generate` items the next run asks for, as the module will pass them to `stillmade.actions.run` (leave the prompt out; the price never reads it); `hosted` names the hosted operations it calls, each once unless `count` says more. Call it when a choice that changes the price changes (the model, shape, size, length, how many, the photos given); StillMade waits until the choices settle and prices them as the run will be priced. The Block never receives or writes the number, the button never moves, and a viewer or a run in progress shows none. `null` takes the price off. The SDK preview shows 0 credits.',where:all},
|
|
18
20
|
{name:'useOutput',group:'Inputs and runs',signature:'useOutput(outputs: Partial<Out>): Promise<Record<string, unknown>>',summary:'Chooses media the view already holds (input media or a desktop recording) as the Block outputs without running. Only complete references the host issued are accepted.',where:saved},
|
|
19
21
|
{name:'schemaFields',group:'Inputs and runs',signature:'readonly schemaFields: Readonly<Record<string, unknown>>',summary:'Helpers that build form fields from the input ports and their schemas, honoring inputs the host locked.',where:all},
|
|
20
22
|
// Shared state
|
|
@@ -33,6 +35,7 @@ export const VIEW_BRIDGE=Object.freeze([
|
|
|
33
35
|
{name:'previewMedia',group:'Media',signature:'previewMedia(value: StillMadeVideo | StillMadeAudio): Promise<{kind: "video" | "audio"; url: string; mimeType: string; bytes: number; blob: Blob}>',summary:'A playable blob: URL for audio or video the view may play, and its bytes (blob) for decoding, such as a waveform. Call releaseMedia when done; a standard view holds two at a time, a frame view 24.',where:all},
|
|
34
36
|
{name:'releaseMedia',group:'Media',signature:'releaseMedia(url: string): void',summary:'Frees a URL previewMedia returned.',where:all},
|
|
35
37
|
{name:'previewVoice',group:'Media',signature:'previewVoice(voice: string): Promise<{url: string}>',summary:'A playable blob: URL of the free sample of a StillMade voice (a voice id from the generation models list), for a Block that declares audio.speech. StillMade reads it; the view has no network.',where:all},
|
|
38
|
+
{name:'download',group:'Media',signature:'download(media: StillMadeImage | StillMadeVideo | StillMadeAudio): Promise<void>',summary:'Saves a picture, clip or sound StillMade gave this screen (its inputs, its outputs, or what `getShared().made` lists) to the person\'s device through StillMade\'s own download, under the export policy (a plan that marks exports gets the mark, or the offer to upgrade). Call it from a click or key press in the view. A `<button data-stillmade-action="download">` does this with no code: it saves the largest picture or clip shown on the `sm-stage` that holds it (one the view showed with `previewImage` or `previewMedia`), so a stage\'s Download sits beside its run button. Refused for media StillMade did not give this screen, Chat, and without a gesture; in the SDK preview the browser saves the file as it is.',where:all},
|
|
36
39
|
{name:'allowSpending',group:'Media',signature:'allowSpending(request: {credits: number, label: string}): Promise<{allowed: boolean, credits: number}>',summary:'From a person\'s press of a button that showed a total, ask StillMade once to approve it: the Step\'s later runs spend from it silently at StillMade\'s prices for 30 minutes. In a project Step only, never from Chat, up to VIEW_LIMITS.allowanceCredits.',where:all},
|
|
37
40
|
{name:'asset',group:'Media',signature:'asset(path: string): string',summary:'A blob: URL for one of the view\'s own asset files, for <img>, new FontFace() or <video>.',where:frame},
|
|
38
41
|
{name:'live',group:'Media',signature:'readonly live: {start(options: {camera?: boolean; microphone?: boolean; fps?: number; width?: number; sampleRate?: number}): Promise<{live: true; camera: boolean; microphone: boolean; fps: number; width: number; height: number; sampleRate: number}>; stop(): Promise<{live: false}>; status(): Promise<{live: boolean; [key: string]: unknown}>; onFrame(callback: (frame: ImageBitmap, info: {index: number; timestamp: number; width: number; height: number}) => void): () => void; onAudio(callback: (samples: Float32Array, info: {index: number; timestamp: number; sampleRate: number}) => void): () => void; onEnd(callback: (event: {reason: string}) => void): () => void}',summary:'The camera, the microphone or both as they happen (declare camera.capture and microphone.capture). Nothing reaches the view until the person presses Start in StillMade, which then shows that the device is on and a Turn off button. Frames arrive as ImageBitmaps, closed when your callback returns, so draw or copy them there; a slow view gets fewer frames, never a backlog. Audio arrives as mono Float32Array chunks. Nothing is saved. In browsers and StillMade Desktop.',where:all},
|
|
@@ -35,7 +35,7 @@ export const BLOCK_KIT_CLASSES=Object.freeze({
|
|
|
35
35
|
'sm-media-off':'A picture or clip that cannot show here (on another device, or it did not open): a quiet tile in its own shape (--sm-photo-ratio, the tile\'s shape in a gallery) holding the image-off glyph, with the reason in aria-label and title and no words inside.',
|
|
36
36
|
'sm-add':'The add tile for an input\'s files: <button class="sm-add" data-stillmade-add="<input>" aria-label="Add photos">. StillMade opens its own picker and saves the files; never an <input type=file>. Each tile of a list can carry .sm-tile-tools with data-stillmade-move and data-stillmade-remove buttons (data-index, data-to).',
|
|
37
37
|
'sm-scrim':'Words over a photo or video, in a positioned box such as an sm-stage: a see-through shade of the page under them, in every theme. var(--scrim) is the same shade for your own rules; never color-mix() or a fixed rgba().',
|
|
38
|
-
'sm-glyph':'A small icon drawn in currentColor: data-glyph="play|pause|plus|close|check|chevron|chevron-left|chevron-right|upload|image|image-off|video|audio|text|spark|info|warning|spinner".',
|
|
38
|
+
'sm-glyph':'A small icon drawn in currentColor: data-glyph="play|pause|plus|close|check|chevron|chevron-left|chevron-right|upload|download|image|image-off|video|audio|text|spark|info|warning|spinner".',
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
// The design principles for every Block interface, in priority order.
|
|
@@ -45,10 +45,10 @@ export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
|
|
|
45
45
|
'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.',
|
|
46
46
|
'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`), and every part shows at once: before the first run the result\'s place is drawn in the shape and size of what is coming (a 9:16 frame for a vertical clip, lines for text, a player bar for sound), so the result fills it instead of popping in. On a wide screen what goes in sits on the left and the result on the right; they stack only on narrow screens and phones. A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. A clip is a `<video controls>`, which StillMade plays in its own player with full screen, and a picture in `sm-stage` or `sm-gallery` (or marked `data-stillmade-expand`) gets StillMade\'s full-screen button; leave out `controls` only when the Block drives playback itself. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
|
|
47
47
|
'One obvious next action: one solid `sm-primary` in the kit\'s ink, never restyled to the background or a ghost outline; peers are `sm-quiet` buttons or named icons, and close is an X icon. Seven or fewer choices in view.',
|
|
48
|
-
'Actions on the stage (owner, November 5, 2026): a job\'s `sm-actions` and `sm-status` go inside its `sm-stage`, and the kit lays them over it, the status top-left and the actions top-right, in the cream blocks of StillMade\'s player, with a band kept above the media so they never cover it; the media fills the rest of the stage. A button that spends credits never moves: the run button (`data-stillmade-action="run"`) is always the rightmost and stays in that spot when it makes again ("Make video", then "Make video again"), and what the result adds (Download, Share) goes to its left. Controls on media (StillMade\'s player, the full-screen square) show for 3 seconds the first time it comes into view, then only while it is hovered or tapped. StillMade\'s own Step pages do the same. An app that lays itself out keeps its own toolbar.',
|
|
48
|
+
'Actions on the stage (owner, November 5, 2026): a job\'s `sm-actions` and `sm-status` go inside its `sm-stage`, and the kit lays them over it, the status top-left and the actions top-right, in the cream blocks of StillMade\'s player, with a band kept above the media so they never cover it; the media fills the rest of the stage. A button that spends credits never moves: the run button (`data-stillmade-action="run"`) is always the rightmost and stays in that spot when it makes again ("Make video", then "Make video again"), and what the result adds (Download, Share) goes to its left. Controls on media (StillMade\'s player, the full-screen square) show for 3 seconds the first time it comes into view, then only while it is hovered or tapped. Under a rendered video the stage shows its shots with their times and the line being said (the video\'s `parts`, which `timeline.render` fills), and the status carries its length. StillMade\'s own Step pages do the same. An app that lays itself out keeps its own toolbar.',
|
|
49
49
|
'Speak the job unless the person or creator asks otherwise. The action button names what it does in the job\'s words (`manifest.action`: "Plan the tour"; AI pictures, video and sound start with Generate and say what, the person\'s own media cut or rendered says Make: "Generate voiceover", "Make 6 clips"), never "Make it", "Run", "Run step", "Submit", "Process" or a bare "Generate"; progress counts the job\'s units ("4 of 9 rooms"), never "Processing"; results and problems use its words in one line. 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 than the one shown.',
|
|
50
50
|
'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.',
|
|
51
|
-
'A picked option reads at a glance: keep the kit\'s ink fill on an `sm-choices` pick, never restyled to an outline, a tint or the page\'s background.',
|
|
51
|
+
'A picked option reads at a glance: keep the kit\'s ink fill on an `sm-choices` pick, never restyled to an outline, a tint or the page\'s background. A picture, or a pick shown by its picture (a thumbnail, a shot, a swatch), is a rounded rectangle in the kit\'s radii, never a circle, unless the person asks for one.',
|
|
52
52
|
'Media that cannot show here (on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape, never words in its place.',
|
|
53
53
|
'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.',
|
|
54
54
|
'Strong defaults; secondary settings go in `sm-more`, closed.',
|
|
@@ -85,7 +85,7 @@ export const APP_INTERFACE_PRINCIPLES=Object.freeze([
|
|
|
85
85
|
'Work happens on the surface: select, drag, zoom and pan, keyboard shortcuts for the common tools, undo and redo.',
|
|
86
86
|
'Speak the craft: tools and panels use the words people of that craft use (Exposure, Keyframe, Extrude, Layers, Mask), never a plain-language rewrite. A button that spends credits still names what it makes ("Generate fill"), carries `data-stillmade-action="run"` and shows no price; StillMade adds it.',
|
|
87
87
|
'Order, not sparseness: tools grouped in panels that fold, the most used in view, rare ones in menus. No intros or paragraphs on the surface; a tool that needs explaining gets a tooltip or an (i).',
|
|
88
|
-
'StillMade\'s appearance (Light, Dark and White) and the Block kit\'s controls (buttons, fields, sliders, `sm-choices` with thumbnails for presets) wherever they fit; the layout is the app\'s own. A look the person names wins.',
|
|
88
|
+
'StillMade\'s appearance (Light, Dark and White) and the Block kit\'s controls (buttons, fields, sliders, `sm-choices` with thumbnails for presets) wherever they fit; the layout is the app\'s own. A look the person names wins. An app may drop StillMade\'s style instead: view `appearance: "none"`, and StillMade adds none of its theme, colors, fonts or Block kit, so the app\'s own CSS styles everything and the style rules (the kit\'s ink fill, its radii, Light, Dark and White) are its own call. Shared state, inputs and outputs, hosted operations, the run button and StillMade\'s picker work as before.',
|
|
89
89
|
'On a phone the work surface stays usable and the panels fold into sheets or one tool strip.',
|
|
90
90
|
'What the person makes leaves the app as the Block\'s outputs (and, while they work, its shared state), so the next Step and the other people in the project get it.',
|
|
91
91
|
]);
|
|
@@ -333,7 +333,8 @@ export const viewWordingNotes=(view,{mode='job'}={})=>view?jobWordWarnings(view,
|
|
|
333
333
|
|
|
334
334
|
// mode: 'job' (the default) or 'app' (interfaceMode(manifest)). An app is held only to the shared checks: its own
|
|
335
335
|
// layout, many controls, labels above fields in an inspector and a text-first surface (a code editor) are what it is.
|
|
336
|
-
|
|
336
|
+
// manifest (optional): the Block's manifest, so a job whose result is media or a document is held to the stage rule.
|
|
337
|
+
export function reviewViewDesign(view,{mode='job',manifest=null}={}){
|
|
337
338
|
const principles=interfacePrinciples(mode);
|
|
338
339
|
if(!view)return {passed:true,issues:[],warnings:[],principles};
|
|
339
340
|
const html=view.html||'',javascript=view.javascript||'',primary=html.replace(/<details\b[\s\S]*?<\/details>/gi,'');
|
|
@@ -341,7 +342,7 @@ export function reviewViewDesign(view,{mode='job'}={}){
|
|
|
341
342
|
if(mode==='app'){
|
|
342
343
|
if(ownFileInput(primary))warnings.push('The view has a file input of its own. Add files with the + tile, <button class="sm-add" data-stillmade-add="<input>" aria-label="Add photos">, which opens StillMade\'s picker.');
|
|
343
344
|
warnings.push(...jobWordWarnings(view,primary,mode));
|
|
344
|
-
warnings.push(...blockKitProblems(view,{mode}).map(problem=>problem.message));
|
|
345
|
+
warnings.push(...blockKitProblems(view,{mode,manifest}).map(problem=>problem.message));
|
|
345
346
|
return {passed:true,issues,warnings,principles};
|
|
346
347
|
}
|
|
347
348
|
const largeTextarea=/<textarea\b[^>]*rows\s*=\s*["']?(?:1[2-9]|[2-9]\d)["']?[^>]*>/i.test(primary);
|
|
@@ -368,7 +369,7 @@ export function reviewViewDesign(view,{mode='job'}={}){
|
|
|
368
369
|
// a margin the view sets), so the warning promises nothing about it.
|
|
369
370
|
const narrow=[...html.matchAll(/\sdata-stillmade-layout\s*=\s*(["']?)([^"'\s>]*)\1/gi)].some(match=>match[2]==='own')?null:narrowScreen(view);
|
|
370
371
|
if(narrow)warnings.push(`The screen is held to ${Math.round(narrow.px)}px wide (${narrow.where}), so a wide Step shows mostly empty space, and StillMade's zoom cannot fill it. Drop the fixed or maximum width: the Block kit lays out the controls and the result across the width.`);
|
|
371
|
-
warnings.push(...blockKitProblems(view).map(problem=>problem.message));
|
|
372
|
+
warnings.push(...blockKitProblems(view,{manifest}).map(problem=>problem.message));
|
|
372
373
|
return {passed:issues.length===0,issues,warnings,principles};
|
|
373
374
|
}
|
|
374
375
|
|
|
@@ -388,15 +389,55 @@ export function unmarkedButtons(view){
|
|
|
388
389
|
|
|
389
390
|
// What a NEW Block's interface must get right. Each problem is {rule, message}.
|
|
390
391
|
// The builder contract's DOD-15 (the SDK's `ready`, StillMade's builder) fails
|
|
391
|
-
// on 'action', 'price' and '
|
|
392
|
-
// 'kit'. Installed Blocks are never refused for these; admission only warns.
|
|
392
|
+
// on 'action', 'price', 'heading' and 'stage' (November 8, 2026: a job's result on its stage, its run button and status
|
|
393
|
+
// on it); StillMade's own builder also repairs 'kit'. Installed Blocks are never refused for these; admission only warns.
|
|
393
394
|
const GENERIC_ACTIONS=/^(?:run|run step|run it|submit|go|start|ok|review cost|review price)$/i;
|
|
394
|
-
export const BLOCK_KIT_READINESS_RULES=Object.freeze(['action','price','heading']);
|
|
395
|
+
export const BLOCK_KIT_READINESS_RULES=Object.freeze(['action','price','heading','stage']);
|
|
395
396
|
// An app (mode 'app') lays itself out, so it is never asked for the kit's page, and only its run button is held to naming
|
|
396
397
|
// what it does: "OK" and "Go" are fine on its dialogs and tools.
|
|
397
|
-
|
|
398
|
+
// November 8, 2026 (owner, of Generate 1.2.0's own grid with its button under the form: "it should have used the SDK that
|
|
399
|
+
// makes the image or video on the right sit in something, like a canvas, with a download button at the top right"): a job
|
|
400
|
+
// whose result is media or a document shows it on an sm-stage, its run button in the stage's own sm-actions and its status
|
|
401
|
+
// in the stage's own sm-status, where the kit lays them over the stage's top corners (Actions on the stage, option 7). Read
|
|
402
|
+
// from the view's markup in one pass over its tags, for a page built from the kit (an sm-block in its markup), standard or
|
|
403
|
+
// frame: a page that is not is held to the 'kit' rule instead, so an original design keeps its own readiness. A frame view
|
|
404
|
+
// that builds its stage in code (its strings name sm-stage) is not checked: only the page it draws shows where the parts are.
|
|
405
|
+
const STAGE_RESULTS=/^(?:image|video|audio|asset|document)(?:\[\])?$/;
|
|
406
|
+
const stageResult=manifest=>{const entries=Object.entries(manifest?.outputs||{}).filter(([,port])=>!port?.context);const port=(entries.find(([,entry])=>entry?.primary)||entries[0]||[])[1];return STAGE_RESULTS.test(String(port?.type||''));};
|
|
407
|
+
const classesOf=tag=>(/\sclass\s*=\s*(["'])([^"']*)\1/i.exec(tag)?.[2]||'').split(/\s+/).filter(Boolean);
|
|
408
|
+
export function stageProblem(view,manifest){
|
|
409
|
+
const html=String(view?.html||'');
|
|
410
|
+
if(!stageResult(manifest)||/\sdata-stillmade-layout\s*=\s*["']?own\b/i.test(html)||!/\bclass\s*=\s*["'][^"']*\bsm-block\b/i.test(html))return null;
|
|
411
|
+
const stack=[];let stages=0,runs=0,runsOnStage=0,statusOnStage=false;
|
|
412
|
+
for(let start=html.indexOf('<');start!==-1;start=html.indexOf('<',start+1)){
|
|
413
|
+
const end=html.indexOf('>',start);if(end===-1)break;
|
|
414
|
+
const tag=html.slice(start+1,end),closing=tag[0]==='/',name=(closing?tag.slice(1):tag).split(/[\s/>]/)[0].toLowerCase();
|
|
415
|
+
if(!/^[a-z][a-z0-9-]*$/.test(name)){start=end;continue;}
|
|
416
|
+
if(closing){let at=stack.length-1;while(at>=0&&stack.length-at<64&&stack[at].name!==name)at--;if(at>=0&&stack[at].name===name)stack.length=at;start=end;continue;}
|
|
417
|
+
if(name==='script'||name==='style'){const close=html.toLowerCase().indexOf(`</${name}`,end);if(close===-1)break;start=close;continue;}
|
|
418
|
+
const classes=classesOf(tag),parent=stack.at(-1),grand=stack.at(-2);
|
|
419
|
+
if(classes.includes('sm-stage'))stages++;
|
|
420
|
+
if(classes.includes('sm-status')&&parent?.stage)statusOnStage=true;
|
|
421
|
+
if(runButton(tag.toLowerCase())){runs++;if(parent?.actions&&grand?.stage)runsOnStage++;}
|
|
422
|
+
if(!VOID_TAGS.has(name)&&!tag.endsWith('/'))stack.push({name,stage:classes.includes('sm-stage'),actions:classes.includes('sm-actions')});
|
|
423
|
+
start=end;
|
|
424
|
+
}
|
|
425
|
+
if(!stages)return /\bsm-stage\b/.test(quotedText(String(view?.javascript||'')))?null:'The result has no stage. Show it on <section class="sm-stage">, a direct child of the sm-block, with the run button in the stage\'s own <div class="sm-actions"> and the status in its <p class="sm-status">: the kit lays them over the stage\'s top corners.';
|
|
426
|
+
const off=[runs>runsOnStage&&'the run button (data-stillmade-action="run") sits outside the stage\'s own <div class="sm-actions">',runs&&!statusOnStage&&'the job\'s <p class="sm-status"> is not a direct child of the stage'].filter(Boolean);
|
|
427
|
+
return off.length?`On the stage, ${off.join(' and ')}. Put the run button in the stage\'s own <div class="sm-actions"> and the status in its own <p class="sm-status">, both direct children of the sm-stage: the kit lays the status over its top-left corner and the actions over its top-right, the run button rightmost so it never moves, Download to its left.`:null;
|
|
428
|
+
}
|
|
429
|
+
const PICTURE_SELECTOR=/\b(?:img|picture|video|canvas)\b|sm-choices|sm-gallery|thumb|tile|swatch|shot|photo|cover|poster|frame/i;
|
|
430
|
+
const ROUND=/border-radius\s*:\s*(?:50%|100%|9{3,}px|[1-9]\d{2,}px)/i;
|
|
431
|
+
// One pass: each rule is the text before a "}", split at its last "{" (selector, declarations).
|
|
432
|
+
const roundPictures=css=>{const text=css.length>65536?css.slice(0,65536):css;for(const chunk of text.split('}')){const open=chunk.lastIndexOf('{');if(open<0)continue;const body=chunk.slice(open+1);if(body.length>4000||!ROUND.test(body))continue;const selector=chunk.slice(Math.max(0,chunk.lastIndexOf('{',open-1)+1,open-2000),open);if(PICTURE_SELECTOR.test(selector))return true;}return false;};
|
|
433
|
+
// manifest: the Block's manifest, when known; a job whose result is media or a document is then held to the stage rule.
|
|
434
|
+
export function blockKitProblems(view,{mode='job',manifest=null}={}){
|
|
398
435
|
if(!view)return [];
|
|
399
|
-
const problems=[],html=String(view.html||''),strings=quotedText(String(view.javascript||'')),original=
|
|
436
|
+
const problems=[],html=String(view.html||''),strings=quotedText(String(view.javascript||'')),original=['original','none'].includes(view.appearance),app=mode==='app';
|
|
437
|
+
// November 7, 2026 (owner, of round shot tiles: "keep it a square rectangle ... make sure it never comes out like that
|
|
438
|
+
// unless explicitly said"): a picture, or something picked by its picture, is a rounded rectangle. An imported design
|
|
439
|
+
// (appearance "original") and an app with its own style ("none") keep their own look.
|
|
440
|
+
if(!original&&roundPictures([view.css,view.themedCss].filter(text=>typeof text==='string').join('\n')))problems.push({rule:'kit',message:'A picture, or a pick shown by its picture, is rounded into a circle. Keep it a rounded rectangle (var(--r-2) or var(--r-3)); a circle only when the person asks for one.'});
|
|
400
441
|
const lower=html.toLowerCase();
|
|
401
442
|
// Each character is read a bounded number of times: the next search starts after this button.
|
|
402
443
|
for(let start=lower.indexOf('<button'),close=0;start!==-1;start=lower.indexOf('<button',close+9)){
|
|
@@ -410,5 +451,8 @@ export function blockKitProblems(view,{mode='job'}={}){
|
|
|
410
451
|
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.'});
|
|
411
452
|
if(!original&&/<h1\b/i.test(html))problems.push({rule:'heading',message:'Drop the page heading; StillMade already shows the Block\'s name.'});
|
|
412
453
|
if(!original&&!app&&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.'});
|
|
454
|
+
// Frame views included: a frame view lays itself out with the same kit.
|
|
455
|
+
const stage=!original&&!app&&manifest?stageProblem(view,manifest):null;
|
|
456
|
+
if(stage)problems.push({rule:'stage',message:stage});
|
|
413
457
|
return problems;
|
|
414
458
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Self-contained: embedded in the trusted, opaque-origin frame bootstrap.
|
|
2
|
-
export function installViewRenderer(document,onError=()=>{},onControlsChanged=()=>{}){
|
|
2
|
+
export function installViewRenderer(document,onError=()=>{},onControlsChanged=()=>{},{frame=false}={}){
|
|
3
3
|
const tags=new Set(['div','section','article','header','footer','p','span','strong','em','i','small','h1','h2','h3','h4','label','button','input','textarea','select','option','ul','ol','li','table','thead','tbody','tr','th','td','details','summary','output','progress','img']);
|
|
4
4
|
const containers=new Set(['DIV','SPAN','SECTION','ARTICLE','MAIN','ASIDE','UL','OL','TBODY']);
|
|
5
5
|
const keys=new Set(['tag','text','children','id','className','action','value','type','checked','disabled','placeholder','min','max','step','label']);
|
|
@@ -33,12 +33,17 @@ export function installViewRenderer(document,onError=()=>{},onControlsChanged=()
|
|
|
33
33
|
if(!value&&by)value=by.split(/\s+/).map(id=>document.getElementById(id)?.textContent||'').join(' ');
|
|
34
34
|
if(!value&&node.labels?.length)value=ownText(node.labels[0]);
|
|
35
35
|
if(!value.trim()&&node.tagName==='BUTTON')value=node.textContent||'';
|
|
36
|
-
|
|
36
|
+
// A frame view's field may say what it is for only in its placeholder ("Write or paste the narration here…").
|
|
37
|
+
return String(value.trim()?value:node.getAttribute('title')||frame&&node.getAttribute('placeholder')||'').replace(/\s+/g,' ').trim().slice(0,240);
|
|
37
38
|
};
|
|
38
39
|
const plainNode=node=>{
|
|
39
40
|
const meta=actions.get(node);if(meta&&!meta.plain)return false;
|
|
40
41
|
if(node.closest('[data-stillmade-share],[data-stillmade-local]'))return false;
|
|
41
|
-
|
|
42
|
+
// A frame view's screen is built by its own code as it runs (a bundled app), so no check can read its buttons before
|
|
43
|
+
// it opens: there a button with words of its own is offered by those words (November 7, 2026: the original
|
|
44
|
+
// Voiceover, Image Panels and Editor screens shipped as frame views gave Chat no button to press). A standard view's
|
|
45
|
+
// buttons are checked when it is built and must be marked.
|
|
46
|
+
if(node.tagName==='BUTTON')return node.hasAttribute('data-stillmade-action')||frame&&!!plainLabel(node);
|
|
42
47
|
if(node.tagName==='INPUT'&&SKIPPED_TYPES.has(node.type))return false;
|
|
43
48
|
return node.hasAttribute('data-stillmade-action')||!!plainLabel(node);
|
|
44
49
|
};
|
|
@@ -53,6 +58,10 @@ export function installViewRenderer(document,onError=()=>{},onControlsChanged=()
|
|
|
53
58
|
};
|
|
54
59
|
const actionOf=node=>{const meta=actions.get(node);return meta.plain?String(node.getAttribute('data-stillmade-action')||node.id||node.getAttribute('name')||plainLabel(node)).slice(0,120):meta.action;};
|
|
55
60
|
const check=(ok,message)=>{if(!ok)throw Error(message);};
|
|
61
|
+
// Set through the element's own setter, as typing does: a screen built with React keeps the last value it saw on the
|
|
62
|
+
// element itself and ignores an input event after a plain assignment (November 7, 2026: Chat's text never reached
|
|
63
|
+
// the original screens running as frame views).
|
|
64
|
+
const assign=(node,key,value)=>{const setter=Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node),key)?.set;if(setter)setter.call(node,value);else node[key]=value;};
|
|
56
65
|
const text=(value,max=10000)=>{check(typeof value==='string'&&value.length<=max,'Dynamic control text exceeds its limit');return value;};
|
|
57
66
|
const textControl=node=>node.tagName==='TEXTAREA'||node.tagName==='INPUT'&&node.type==='text';
|
|
58
67
|
const emit=(node,root,name)=>{
|
|
@@ -189,15 +198,19 @@ export function installViewRenderer(document,onError=()=>{},onControlsChanged=()
|
|
|
189
198
|
const plainChanged=()=>{plainTimer=null;if(disposed||plainSignature===null)return;let next;try{next=plainState();}catch{return;}if(next!==plainSignature){plainSignature=next;revision++;notify();}};
|
|
190
199
|
const observer=Observer?new Observer(()=>{if(plainTimer===null&&!disposed)plainTimer=later(plainChanged,250);}):null;
|
|
191
200
|
observer?.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['disabled','hidden','class','data-stillmade-action','aria-label']});
|
|
192
|
-
const inspectControls=({offset=0,limit=20}={})=>{check(!disposed,'Dynamic controls are closed');check(Number.isSafeInteger(offset)&&offset>=0&&Number.isSafeInteger(limit)&&limit>=1&&limit<=20,'Choose a bounded control page');const nodes=currentControls();plainSignature=plainState();check(offset<=nodes.length,'Choose a current control page');return {revision,total:nodes.length,offset,nextOffset:offset+limit<nodes.length?offset+limit:null,controls:nodes.slice(offset,offset+limit).map(node=>{const meta=actions.get(node),value=String(node.value??'');return {id:meta.handle,action:actionOf(node),label:label(node),tag:node.tagName.toLowerCase(),type:node.type,event:meta.event,disabled:node.disabled,hidden:hidden(node),value:value.slice(0,240),valuePartial:value.length>240,...(typeof node.checked==='boolean'?{checked:node.checked}:{}),...(node.tagName==='SELECT'?{choices:choices(node).slice(0,40),choicesPartial:node.options.length>40}:{}),identity:identity(node)};})};};
|
|
201
|
+
const inspectControls=({offset=0,limit=20}={})=>{check(!disposed,'Dynamic controls are closed');check(Number.isSafeInteger(offset)&&offset>=0&&Number.isSafeInteger(limit)&&limit>=1&&limit<=20,'Choose a bounded control page');const nodes=currentControls();plainSignature=plainState();check(offset<=nodes.length,'Choose a current control page');return {revision,total:nodes.length,offset,nextOffset:offset+limit<nodes.length?offset+limit:null,controls:nodes.slice(offset,offset+limit).map(node=>{const meta=actions.get(node),value=String(node.value??'');return {id:meta.handle,action:actionOf(node),label:label(node),tag:node.tagName.toLowerCase(),type:node.type,event:meta.event,disabled:node.disabled,hidden:hidden(node),value:value.slice(0,240),valuePartial:value.length>240,...(typeof node.checked==='boolean'?{checked:node.checked}:{}),...(node.tagName==='SELECT'?{choices:choices(node).slice(0,40),choicesPartial:node.options.length>40}:{}),...(modal(node)?{modal:true}:{}),...(primary(node)?{primary:true}:{}),identity:identity(node)};})};};
|
|
202
|
+
// A control inside an open dialog of the screen (aria-modal or <dialog open>), and the screen's main button: a run StillMade
|
|
203
|
+
// presses for the person answers the dialog, with its recommended option, before anything behind it (November 7, 2026).
|
|
204
|
+
const modal=node=>!!node.closest?.('[aria-modal="true"],dialog[open]');
|
|
205
|
+
const primary=node=>!!(node.autofocus||node.classList?.contains('btn-primary')||node.classList?.contains('sm-primary')||node.hasAttribute?.('data-stillmade-primary'));
|
|
193
206
|
const dispatchControl=async(args,settle=async()=>{})=>{
|
|
194
207
|
check(!dispatching,'A declared control is already running');assertCommitted();
|
|
195
208
|
check(args&&typeof args==='object'&&!Array.isArray(args)&&Object.keys(args).every(key=>['id','revision','identity','value','checked'].includes(key))&&typeof args.id==='string'&&args.revision===revision&&typeof args.identity==='string','The declared control changed');
|
|
196
209
|
const node=currentControls().find(node=>actions.get(node).handle===args.id),meta=node&&actions.get(node);
|
|
197
210
|
check(node&&identity(node)===args.identity,'The declared control changed');check(!node.disabled&&!hidden(node),'This declared control is unavailable');check(meta.plain||listeners.size>0,'This declared control has no native handler');
|
|
198
211
|
if(node.tagName==='BUTTON')check(!Object.hasOwn(args,'value')&&!Object.hasOwn(args,'checked'),'Buttons take no input value');
|
|
199
|
-
else if(['checkbox','radio'].includes(node.type)){check(typeof args.checked==='boolean'&&!Object.hasOwn(args,'value'),'Choose a checkbox value');node
|
|
200
|
-
else{check((typeof args.value==='string'&&args.value.length<=10000||typeof args.value==='number'&&Number.isFinite(args.value))&&!Object.hasOwn(args,'checked'),'Choose a bounded control value');if(node.tagName==='SELECT')check(Array.from(node.options).some(option=>option.value===String(args.value)),'Choose a declared option');node
|
|
212
|
+
else if(['checkbox','radio'].includes(node.type)){check(typeof args.checked==='boolean'&&!Object.hasOwn(args,'value'),'Choose a checkbox value');assign(node,'checked',args.checked);}
|
|
213
|
+
else{check((typeof args.value==='string'&&args.value.length<=10000||typeof args.value==='number'&&Number.isFinite(args.value))&&!Object.hasOwn(args,'checked'),'Choose a bounded control value');if(node.tagName==='SELECT')check(Array.from(node.options).some(option=>option.value===String(args.value)),'Choose a declared option');assign(node,'value',String(args.value));}
|
|
201
214
|
const payload={action:meta.action,value:node.value,checked:node.checked,event:meta.event};
|
|
202
215
|
dispatching=true;lockControls(true);
|
|
203
216
|
if(meta.plain)try{if(node.tagName==='BUTTON')node.click();else for(const type of ['input','change'])node.dispatchEvent(new document.defaultView.Event(type,{bubbles:true}));check(!disposed,'Dynamic controls are closed');await settle();check(!disposed,'Dynamic controls are closed');return {settled:true};}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {parse} from 'acorn';
|
|
2
2
|
import {assert,plain} from './contracts.js';
|
|
3
3
|
import {moduleFileDigest} from './module-runtime.js';
|
|
4
|
+
import {VIEW_APPEARANCES} from './appearance.js';
|
|
4
5
|
|
|
5
6
|
export const VIEW_LIMITS=Object.freeze({htmlBytes:65536,cssBytes:32768,javascriptBytes:65536,messageBytes:4194304,runsPerMinute:120,imagePreviewsPerMinute:240,voicePreviewsPerMinute:30,allowanceCredits:2000,imagePreviewMs:15000,imagePreviewEdge:512,imagePreviewMaxEdge:2048});
|
|
6
7
|
const bytes=value=>new TextEncoder().encode(value).length;
|
|
@@ -63,7 +64,7 @@ function promptDataAccess(ast){
|
|
|
63
64
|
export function validateView(view){
|
|
64
65
|
if(isFrameView(view))return validateFrameView(view);
|
|
65
66
|
assert(plain(view) && Object.keys(view).every(key=>['html','css','javascript','themedCss','appearance'].includes(key)),'VIEW','View must contain only html, css, javascript, optional themedCss, and appearance');
|
|
66
|
-
assert(view.appearance===undefined ||
|
|
67
|
+
assert(view.appearance===undefined || VIEW_APPEARANCES.includes(view.appearance),'VIEW','View appearance must be host, original or none');
|
|
67
68
|
for(const field of ['html','css','javascript','themedCss']){
|
|
68
69
|
if(field!=='html' && view[field]===undefined)continue;
|
|
69
70
|
assert(typeof view[field]==='string' && bytes(view[field])<=VIEW_LIMITS[`${field==='themedCss'?'css':field}Bytes`] && (field!=='html' || view[field].trim().length>0),'VIEW',`View ${field} exceeds its size limit or is empty`);
|
|
@@ -181,7 +182,7 @@ export function compileFrameViewCode(source,path,guard){
|
|
|
181
182
|
|
|
182
183
|
export function validateFrameView(view){
|
|
183
184
|
assert(Object.keys(view).every(key=>FRAME_KEYS.includes(key)),'VIEW','A frame view contains runtime "frame", html and optional css, themedCss, javascript, appearance and files');
|
|
184
|
-
assert(view.appearance===undefined ||
|
|
185
|
+
assert(view.appearance===undefined || VIEW_APPEARANCES.includes(view.appearance),'VIEW','View appearance must be host, original or none');
|
|
185
186
|
for(const field of ['html','css','javascript','themedCss']){
|
|
186
187
|
if(field!=='html' && view[field]===undefined)continue;
|
|
187
188
|
const limit=FRAME_VIEW_LIMITS[`${field==='themedCss'?'css':field}Bytes`];
|
|
@@ -38,8 +38,11 @@
|
|
|
38
38
|
the result fills the rest on the right. Under 860px everything stacks in page order, so
|
|
39
39
|
the stage goes after the actions: the result's place under the button. data-layout="stack" opts out; "split" keeps its older layout. */
|
|
40
40
|
@container (min-width:860px){
|
|
41
|
-
:
|
|
41
|
+
/* November 8, 2026: the stage spans twelve rows, so a row gap between the empty ones made it, and the page, taller than
|
|
42
|
+
the controls by up to eleven gaps (Generate's strip fell under a laptop's fold). The controls are spaced by a margin. */
|
|
43
|
+
:where(.sm-block:not([data-layout]):has(> .sm-stage)){grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;column-gap:var(--s-5,24px);row-gap:0}
|
|
42
44
|
:where(.sm-block:not([data-layout]):has(> .sm-stage)) > :where(*){grid-column:1}
|
|
45
|
+
:where(.sm-block:not([data-layout]):has(> .sm-stage)) > :where(:not(.sm-stage) ~ :not(.sm-stage)){margin-top:var(--s-4,16px)}
|
|
43
46
|
:where(.sm-block:not([data-layout]):has(> .sm-stage)) > :where(.sm-stage){grid-column:2;grid-row:1 / span 12;align-self:stretch;min-height:min(70vh,640px)}
|
|
44
47
|
/* A stage with a real shape keeps it: a min-height would widen a 16:9 stage past its column. */
|
|
45
48
|
:where(.sm-block:not([data-layout]):has(> .sm-stage)) > :where(.sm-stage[data-ratio]){align-self:start;min-height:0}
|
|
@@ -302,6 +305,8 @@
|
|
|
302
305
|
:where(.sm-glyph[data-glyph="chevron-right"])::before{left:24%;top:30%;width:40%;height:40%;border:solid currentColor;border-width:2px 2px 0 0;transform:rotate(45deg)}
|
|
303
306
|
: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}
|
|
304
307
|
: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%)}
|
|
308
|
+
:where(.sm-glyph[data-glyph="download"])::before{left:10%;right:10%;bottom:10%;height:35%;border:2px solid currentColor;border-top:0;border-radius:0 0 3px 3px}
|
|
309
|
+
:where(.sm-glyph[data-glyph="download"])::after{left:25%;right:25%;top:4%;height:62%;background:currentColor;clip-path:polygon(40% 0,60% 0,60% 55%,100% 55%,50% 100%,0 55%,40% 55%)}
|
|
305
310
|
:where(.sm-glyph[data-glyph="image"])::before{inset:12%;border:2px solid currentColor;border-radius:3px}
|
|
306
311
|
: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%)}
|
|
307
312
|
:where(.sm-glyph[data-glyph="image-off"])::before{inset:12%;border:2px solid currentColor;border-radius:3px;background:radial-gradient(circle calc(var(--g) * .09) at 35% 35%,currentColor 96%,transparent 100%)}
|
|
@@ -340,8 +345,12 @@
|
|
|
340
345
|
:where(.sm-stage) > :where(.sm-actions) > :where(.sm-quiet,.sm-icon){border-color:transparent;background:var(--cream-100,#F7F0DB);color:var(--ink-600,#0A0A0A)}
|
|
341
346
|
:where(.sm-stage) > :where(.sm-status){position:absolute;top:var(--s-3,12px);left:var(--s-3,12px);z-index:2;display:inline-flex;align-items:center;gap:var(--s-2,8px);min-height:32px;margin:0;padding:0 var(--s-3,12px);border-radius:var(--r-3,8px);background:var(--cream-100,#F7F0DB);color:var(--ink-600,#0A0A0A);font-size:12px}
|
|
342
347
|
:where(.sm-stage) > :where(.sm-status:empty){display:none}
|
|
348
|
+
/* November 8, 2026: on a narrow stage a long run button wraps to two lines (with its price, "Generate 2 images again ·
|
|
349
|
+
4 credits"), and the band kept above the media was one button tall, so the button covered the picture. There the status
|
|
350
|
+
and the actions are the stage's first rows, in its flow: the status, then the actions on the right, as tall as they are. */
|
|
343
351
|
@container (max-width:420px){
|
|
344
|
-
:where(.sm-stage:has(> .sm-actions
|
|
345
|
-
:where(.sm-stage
|
|
352
|
+
:where(.sm-stage:has(> .sm-actions,> .sm-status)){padding-top:var(--s-3,12px)}
|
|
353
|
+
:where(.sm-stage) > :where(.sm-status){position:static;order:-2;justify-self:start;margin-bottom:var(--s-2,8px)}
|
|
354
|
+
:where(.sm-stage) > :where(.sm-actions){position:static;order:-1;justify-self:end;max-width:100%;margin-bottom:var(--s-3,12px)}
|
|
346
355
|
:where(.sm-stage) > :where(.sm-actions) > :where(.sm-primary){flex:0 1 auto}
|
|
347
356
|
}
|