stillmade-sdk 0.1.41 → 0.1.42
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/.stillmade/sdk-baseline.json +19 -19
- package/AGENTS.md +2 -2
- package/INTERFACE_DESIGN.md +5 -0
- package/PASTE_TO_LLM.md +12 -12
- package/SDK.md +44 -23
- package/UI_AESTHETIC.md +8 -1
- package/examples/check-clip.stillmade.json +4 -0
- package/examples/clean-text.stillmade.json +4 -0
- package/examples/custom-interface.stillmade.json +4 -0
- package/examples/describe-image.stillmade.json +4 -0
- package/examples/foley-cue.stillmade.json +4 -0
- package/examples/generate-image.stillmade.json +4 -0
- package/examples/generate-video.stillmade.json +4 -0
- package/examples/module-word-stats/stillmade.block.json +4 -0
- package/examples/narrate-script.stillmade.json +4 -0
- package/examples/numbered-takes.stillmade.json +4 -0
- package/examples/research-topic.stillmade.json +4 -0
- package/examples/score-a-scene.stillmade.json +4 -0
- package/examples/script-draft.stillmade.json +4 -0
- package/examples/summarize-page.stillmade.json +4 -0
- package/examples/text-workflow.stillmade.json +4 -0
- package/examples/thicken-lines.stillmade.json +4 -0
- package/examples/timeline-polish.stillmade.json +4 -0
- package/examples/transcribe-audio.stillmade.json +4 -0
- package/examples/word-count.stillmade.json +4 -0
- package/package.json +1 -1
- package/packages/block-cli/dev-host/assets/host.js +6 -1
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-cli/ready-gate.js +4 -0
- package/packages/block-cli/templates.js +7 -7
- package/packages/block-platform/block-readiness.js +3 -1
- package/packages/block-sdk/audio-generation-example.js +2 -2
- package/packages/block-sdk/builder-contract.js +3 -3
- package/packages/block-sdk/capability-example.js +1 -1
- package/packages/block-sdk/examples.js +3 -3
- package/packages/block-sdk/image-description-example.js +1 -1
- package/packages/block-sdk/image-generation-example.js +1 -1
- package/packages/block-sdk/media-analysis-example.js +1 -1
- package/packages/block-sdk/module-example.js +1 -1
- package/packages/block-sdk/speech-example.js +1 -1
- package/packages/block-sdk/state-example.js +1 -1
- package/packages/block-sdk/timeline-proposal-example.js +1 -1
- package/packages/block-sdk/transcription-example.js +1 -1
- package/packages/block-sdk/trigger-examples.js +2 -2
- package/packages/block-sdk/video-generation-example.js +1 -1
- package/packages/block-sdk/view-design.js +27 -1
- package/packages/block-sdk/web-example.js +2 -2
- package/packages/design-system/block-kit.css +5 -0
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Portable image request. This fixture describes a result to review; it is not
|
|
3
3
|
// evidence of a live generation. Model, quality and payment belong to the host.
|
|
4
4
|
export const imageGenerate={
|
|
5
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.generate-image',version:'1.0.0',name:'Generate an image',description:'Generate a square PNG from a short visual prompt using an approved host request.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{prompt:{type:'text',primary:true,semantic:'image_prompt',required:true,description:'Visual description of the image to generate.'}},outputs:{image:{type:'image',primary:true,semantic:'generated_image',description:'The generated square PNG image.'}},permissions:{project:[],capabilities:['image.generate'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'prompt'}]},
|
|
5
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.generate-image',action:{label:'Generate image',running:'Generating image…'},version:'1.0.0',name:'Generate an image',description:'Generate a square PNG from a short visual prompt using an approved host request.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{prompt:{type:'text',primary:true,semantic:'image_prompt',required:true,description:'Visual description of the image to generate.'}},outputs:{image:{type:'image',primary:true,semantic:'generated_image',description:'The generated square PNG image.'}},permissions:{project:[],capabilities:['image.generate'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'prompt'}]},
|
|
6
6
|
capability:{schemaVersion:1,operation:'image.generate',prompt:{$input:'prompt'},output:'image'},
|
|
7
7
|
tests:[{name:'A simple forest illustration',input:{prompt:'A simple illustration of a quiet forest with clear dark outlines and soft green colors.'},expectations:{image:{kind:'image',format:'png',width:1024,height:1024,minBytes:67,maxBytes:12000000}}}],
|
|
8
8
|
};
|
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Ask StillMade's media vision about a saved video, optionally inside one time
|
|
3
3
|
// range. Each analyzed image or video is one priced call on StillMade credits.
|
|
4
4
|
export const mediaAnalysis={
|
|
5
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.check-clip',version:'1.0.0',name:'Check a clip',description:'Answer a production question about a saved video clip, with timestamps.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
5
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.check-clip',action:{label:'Check clip',running:'Checking clip…'},version:'1.0.0',name:'Check a clip',description:'Answer a production question about a saved video clip, with timestamps.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
6
6
|
inputs:{clip:{type:'video',primary:true,semantic:'source_video',required:true,description:'Saved video to check.'},start:{type:'number',required:false,min:0,semantic:'range_start',description:'Optional start of the range to look at closely, in seconds.'},end:{type:'number',required:false,min:0,semantic:'range_end',description:'Optional end of the range, in seconds (at most 12 seconds after the start).'}},
|
|
7
7
|
outputs:{report:{type:'json',primary:true,semantic:'media_findings',description:'Summary, timeline, on-screen text and issues found.'}},
|
|
8
8
|
permissions:{project:[],capabilities:['media.analyze'],network:[],filesystem:[],secrets:[]},ui:[{control:'asset',port:'clip'},{control:'number',port:'start'},{control:'number',port:'end'}]},
|
|
@@ -12,7 +12,7 @@ export default async function run(input, stillmade) {
|
|
|
12
12
|
}
|
|
13
13
|
`;
|
|
14
14
|
export const moduleWordStats={
|
|
15
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.word-stats',version:'1.0.0',name:'Word stats',description:'Counts words and finds the longest one, as a modern JavaScript module.',kind:'task',runtime:'module',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
15
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.word-stats',action:{label:'Count words',running:'Counting words…'},version:'1.0.0',name:'Word stats',description:'Counts words and finds the longest one, as a modern JavaScript module.',kind:'task',runtime:'module',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
16
16
|
inputs:{text:{type:'text',primary:true,semantic:'plain_text',required:true,description:'Text to measure.'}},
|
|
17
17
|
outputs:{words:{type:'integer',semantic:'word_count',description:'Number of words.'},longest:{type:'text',primary:true,semantic:'longest_word',description:'The longest word.'}},
|
|
18
18
|
permissions:{project:[],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'text'}]},
|
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Portable speech request. These fixtures are expectations to review, not
|
|
3
3
|
// evidence that a provider has generated audio. Host controls choose the voice.
|
|
4
4
|
export const audioSpeech={
|
|
5
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.narrate-script',version:'1.0.0',name:'Narrate a script',description:'Turn a short script into a reviewed speech recording using a host-selected voice.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{script:{type:'script',primary:true,semantic:'narration_script',required:true,description:'Narration script to render as speech.'}},outputs:{audio:{type:'audio',primary:true,semantic:'speech_audio',description:'Speech audio generated from the narration script.'}},permissions:{project:[],capabilities:['audio.speech'],network:[],filesystem:[],secrets:[]},ui:[]},
|
|
5
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.narrate-script',action:{label:'Generate voiceover',running:'Generating voiceover…'},version:'1.0.0',name:'Narrate a script',description:'Turn a short script into a reviewed speech recording using a host-selected voice.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{script:{type:'script',primary:true,semantic:'narration_script',required:true,description:'Narration script to render as speech.'}},outputs:{audio:{type:'audio',primary:true,semantic:'speech_audio',description:'Speech audio generated from the narration script.'}},permissions:{project:[],capabilities:['audio.speech'],network:[],filesystem:[],secrets:[]},ui:[]},
|
|
6
6
|
capability:{schemaVersion:1,operation:'audio.speech',text:{$input:'script'},output:'audio'},
|
|
7
7
|
tests:[{name:'Read a short welcome',input:{script:{schemaVersion:1,text:'Welcome to the quiet forest.'}},expectations:{audio:{kind:'audio',format:'wav',minDuration:0.1,maxDuration:30,minBytes:46,maxBytes:1500000}}}],
|
|
8
8
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
2
2
|
export const numberedTakes={
|
|
3
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.numbered-takes',version:'1.0.0',name:'Numbered takes',description:'Label successive creative takes with an independent counter for each production placement.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{text:{type:'text',primary:true,semantic:'draft_text',required:true,description:'Creative take text to label with the next number.'}},outputs:{text:{type:'text',primary:true,semantic:'numbered_take',description:'The supplied take prefixed with its saved sequence number.'}},permissions:{project:['state.step']},state:{scope:'step',version:1,initial:{count:0}},ui:[{port:'text',control:'text'}]},
|
|
3
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.numbered-takes',action:{label:'Number takes',running:'Numbering takes…'},version:'1.0.0',name:'Numbered takes',description:'Label successive creative takes with an independent counter for each production placement.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{text:{type:'text',primary:true,semantic:'draft_text',required:true,description:'Creative take text to label with the next number.'}},outputs:{text:{type:'text',primary:true,semantic:'numbered_take',description:'The supplied take prefixed with its saved sequence number.'}},permissions:{project:['state.step']},state:{scope:'step',version:1,initial:{count:0}},ui:[{port:'text',control:'text'}]},
|
|
4
4
|
code:'state.count += 1; return {text: "Take " + state.count + ": " + input.text.trim()};',
|
|
5
5
|
tests:[{name:'First take',input:{text:' Opening scene '},expected:{text:'Take 1: Opening scene'},expectedState:{version:1,value:{count:1}}},{name:'Continues saved numbering',input:{text:'Alternate ending'},state:{version:1,value:{count:4}},expected:{text:'Take 5: Alternate ending'},expectedState:{version:1,value:{count:5}}}]
|
|
6
6
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
2
2
|
|
|
3
3
|
export const timelinePolish={
|
|
4
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.timeline-polish',version:'1.0.0',name:'Timeline polish',description:'Turn a timeline and editing direction into a bounded proposal that remains reviewable before Apply.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{timeline:{type:'timeline',context:'timeline',primary:true},direction:{type:'text',default:'Polish this cut conservatively.'}},outputs:{edit:{type:'timeline-edit',primary:true}},permissions:{project:['context.timeline.read','timeline.propose'],capabilities:['timeline.propose'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'direction'}]},
|
|
4
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.timeline-polish',action:{label:'Suggest edit',running:'Suggesting edit…'},version:'1.0.0',name:'Timeline polish',description:'Turn a timeline and editing direction into a bounded proposal that remains reviewable before Apply.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{timeline:{type:'timeline',context:'timeline',primary:true},direction:{type:'text',default:'Polish this cut conservatively.'}},outputs:{edit:{type:'timeline-edit',primary:true}},permissions:{project:['context.timeline.read','timeline.propose'],capabilities:['timeline.propose'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'direction'}]},
|
|
5
5
|
capability:{schemaVersion:1,operation:'timeline.propose',context:{timeline:{$input:'timeline'},direction:{$input:'direction'}},instruction:'Use only supported timeline-edit commands, exact before snapshots, and the current activeTimelineId. Preserve media and locked items. Return strict JSON only.',maxTokens:2000,output:'edit'},
|
|
6
6
|
tests:[{name:'one restrained edit',input:{timeline:{schemaVersion:1,activeTimelineId:'timeline-1',tracks:[{id:'video'}],clips:[{id:'clip-1',trackId:'video',start:0,duration:4}],audioItems:[],captions:[]},direction:'Add a soft transition.'},expectations:{edit:{kind:'timeline-edit',minCommands:1,maxCommands:10}}}],
|
|
7
7
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
2
2
|
|
|
3
3
|
export const audioTranscription={
|
|
4
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.transcribe-audio',version:'1.0.0',name:'Transcribe audio',description:'Turn one persistent audio file into reviewed text with word timings.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{audio:{type:'audio',primary:true},language:{type:'text',default:'auto'}},outputs:{transcript:{type:'transcript',primary:true}},permissions:{project:[],capabilities:['audio.transcribe'],network:[],filesystem:[],secrets:[]},ui:[{control:'asset',port:'audio'},{control:'select',port:'language',options:['auto','en','es','fr','de','it','pt','ja','ko','zh']}]},
|
|
4
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.transcribe-audio',action:{label:'Transcribe audio',running:'Transcribing…'},version:'1.0.0',name:'Transcribe audio',description:'Turn one persistent audio file into reviewed text with word timings.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{audio:{type:'audio',primary:true},language:{type:'text',default:'auto'}},outputs:{transcript:{type:'transcript',primary:true}},permissions:{project:[],capabilities:['audio.transcribe'],network:[],filesystem:[],secrets:[]},ui:[{control:'asset',port:'audio'},{control:'select',port:'language',options:['auto','en','es','fr','de','it','pt','ja','ko','zh']}]},
|
|
5
5
|
capability:{schemaVersion:1,operation:'audio.transcribe',audio:{$input:'audio'},language:{$input:'language'},output:'transcript'},
|
|
6
6
|
tests:[{name:'timed speech',input:{audio:{kind:'audio',assetId:'audio-1',versionId:'version-1',url:'/api/media/example/audio.mp3',mimeType:'audio/mpeg'},language:'en'},expectations:{transcript:{kind:'transcript',minWords:1,maxWords:1000}}}]
|
|
7
7
|
};
|
|
@@ -5,7 +5,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
5
5
|
// credits per call, total credits, expiry), every new video saved to the
|
|
6
6
|
// project is transcribed unattended into timed captions.
|
|
7
7
|
export const autoCaptions={
|
|
8
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.auto-captions',version:'1.0.0',name:'Auto-caption new uploads',description:'Transcribe each new video into timed captions, automatically within the budget the project owner approves.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
8
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.auto-captions',action:{label:'Make captions',running:'Making captions…'},version:'1.0.0',name:'Auto-caption new uploads',description:'Transcribe each new video into timed captions, automatically within the budget the project owner approves.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
9
9
|
inputs:{video:{type:'video',primary:true,required:true,semantic:'source_video',description:'The new video to caption.'},language:{type:'text',default:'auto',required:false,semantic:'language_code',description:'auto, or a two-letter language code such as en.'}},
|
|
10
10
|
outputs:{captions:{type:'transcript',primary:true,semantic:'timed_captions',description:'The spoken words with their timings.'}},
|
|
11
11
|
permissions:{project:[],capabilities:['audio.transcribe'],network:[],filesystem:[],secrets:[]},
|
|
@@ -19,7 +19,7 @@ export const autoCaptions={
|
|
|
19
19
|
// turns it into a short production note. It runs in StillMade's sandbox with
|
|
20
20
|
// no network and no credits.
|
|
21
21
|
export const orderNote={
|
|
22
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.order-note',version:'1.0.0',name:'Order to production note',description:'Turn an order a shop sends by webhook into a short production note.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
22
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.order-note',action:{label:'Write order note',running:'Writing order note…'},version:'1.0.0',name:'Order to production note',description:'Turn an order a shop sends by webhook into a short production note.',kind:'task',runtime:'javascript',license:'MIT',provenance:EXAMPLE_PROVENANCE,
|
|
23
23
|
inputs:{event:{type:'object',semantic:'workflow_event',schema:{id:'string',kind:'string',occurredAt:'string',preview:'boolean',data:'object'},sources:['project'],required:true,description:'The webhook event StillMade received.'}},
|
|
24
24
|
outputs:{note:{type:'text',primary:true,semantic:'production_note',description:'A one-line note for the team.'}},
|
|
25
25
|
permissions:{project:[],network:[],filesystem:[],secrets:[]},
|
|
@@ -2,7 +2,7 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
2
2
|
// Declarative request; the host chooses provider, model, duration, payment and
|
|
3
3
|
// storage. This fixture is executed only after explicit host approval.
|
|
4
4
|
export const videoGenerate={
|
|
5
|
-
manifest:{schemaVersion:1,id:'example.generate-video',name:'Generate video',version:'1.0.0',sdkVersion:'0.1.0',description:'Turn a production prompt into a short video using an approved host provider.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{prompt:{type:'text',primary:true,required:true}},outputs:{video:{type:'video',primary:true}},permissions:{project:[],capabilities:['video.generate'],network:[],filesystem:[],secrets:[]},ui:[{port:'prompt',control:'text'}]},
|
|
5
|
+
manifest:{schemaVersion:1,id:'example.generate-video',action:{label:'Generate video',running:'Generating video…'},name:'Generate video',version:'1.0.0',sdkVersion:'0.1.0',description:'Turn a production prompt into a short video using an approved host provider.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{prompt:{type:'text',primary:true,required:true}},outputs:{video:{type:'video',primary:true}},permissions:{project:[],capabilities:['video.generate'],network:[],filesystem:[],secrets:[]},ui:[{port:'prompt',control:'text'}]},
|
|
6
6
|
capability:{schemaVersion:1,operation:'video.generate',prompt:{$input:'prompt'},output:'video'},
|
|
7
7
|
tests:[{name:'Produces a bounded MP4 sample',input:{prompt:'A paper boat floating across a calm blue pond. Gentle camera movement.'},expectations:{video:{kind:'video',format:'mp4',minDuration:1,maxDuration:60,minBytes:32,maxBytes:67108864,minWidth:1,maxWidth:3840,minHeight:1,maxHeight:3840}}}],
|
|
8
8
|
};
|
|
@@ -46,7 +46,7 @@ export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
|
|
|
46
46
|
'Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them.',
|
|
47
47
|
'Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks with the result first and the action within reach. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out.',
|
|
48
48
|
'Size with the kit, never fixed sizes: no fixed pixel widths or heights for the workspace or stage, no 100vh, no position:fixed panels. StillMade zooms a Block\'s screen to fill the space it has (up to 150%, never down to a phone layout on a computer), so a Block laid out with the kit\'s spacing fills its Step without empty space, and it centers a standard view narrower than its Step. An app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys.',
|
|
49
|
-
'Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles.',
|
|
49
|
+
'Group what the person sets in one `sm-fields` (or `sm-settings` for short ones): the kit gives the group its own surface, so the inputs, the settings and the result read as separate parts. Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles.',
|
|
50
50
|
'Lists fill the width at a small size: input files are small tiles (`sm-gallery data-size="small"`), many items are one line each (`sm-list`): a thumbnail, one line of text, a short fact. Tapping an item opens all of its fields in place. Extra width means more items per row, never bigger cards.',
|
|
51
51
|
'On a phone chip and segment rows scroll sideways instead of wrapping; long lists show their first three with "N more" (`data-peek`) and long text five lines until tapped (`sm-clamp`).',
|
|
52
52
|
'Nothing is lost when a Block gets smaller: every control stays on the screen, behind an (i), in `sm-more`, or inside the item it belongs to. Compacting moves controls; it never deletes them.',
|
|
@@ -248,6 +248,30 @@ function quotedText(javascript){
|
|
|
248
248
|
}
|
|
249
249
|
return strings;
|
|
250
250
|
}
|
|
251
|
+
// November 3, 2026 (owner, of a Step whose settings floated on the page: "make sure it's more separated"): what the person
|
|
252
|
+
// sets sits in one group (.sm-fields, or .sm-settings for short ones), which the kit puts on its own surface. Settings
|
|
253
|
+
// loose on the page are counted in one pass over the tags, never by a regular expression that could backtrack.
|
|
254
|
+
const SETTING_GROUPS=/\b(?:sm-fields|sm-settings|sm-more|sm-list|sm-actions)\b/;
|
|
255
|
+
export function looseFields(html){
|
|
256
|
+
const stack=[],source=String(html||'');let loose=0,grouped=0;
|
|
257
|
+
for(let start=source.indexOf('<');start!==-1;start=source.indexOf('<',start+1)){
|
|
258
|
+
const end=source.indexOf('>',start);if(end===-1)break;
|
|
259
|
+
const tag=source.slice(start+1,end),closing=tag[0]==='/',name=(closing?tag.slice(1):tag).split(/[\s/>]/)[0].toLowerCase();
|
|
260
|
+
if(!/^[a-z][a-z0-9-]*$/.test(name)){start=end;continue;}
|
|
261
|
+
if(closing){
|
|
262
|
+
// Close back to the matching tag, usually the top of the stack; an unmatched close is ignored.
|
|
263
|
+
let at=stack.length-1;while(at>=0&&stack.length-at<64&&stack[at].name!==name)at--;
|
|
264
|
+
if(at>=0&&stack[at].name!==name)at=-1;
|
|
265
|
+
if(at!==-1)while(stack.length>at){if(stack.pop().grouped)grouped--;}
|
|
266
|
+
start=end;continue;
|
|
267
|
+
}
|
|
268
|
+
const classes=/\bclass\s*=\s*(["'])([^"']*)\1/i.exec(tag)?.[2]||'',group=name==='fieldset'&&!/\bsm-choices\b/.test(classes)||SETTING_GROUPS.test(classes);
|
|
269
|
+
if(/(?:^|\s)sm-field(?:\s|$)/.test(classes)&&!grouped)loose++;
|
|
270
|
+
if(!VOID_TAGS.has(name)&&!tag.endsWith('/')){stack.push({name,grouped:group});if(group)grouped++;}
|
|
271
|
+
start=end;
|
|
272
|
+
}
|
|
273
|
+
return loose;
|
|
274
|
+
}
|
|
251
275
|
function jobWordWarnings(view,primary){
|
|
252
276
|
const warnings=[],lower=primary.toLowerCase();
|
|
253
277
|
for(let start=lower.indexOf('<button'),close=0;start!==-1;start=lower.indexOf('<button',close+9)){
|
|
@@ -261,6 +285,8 @@ function jobWordWarnings(view,primary){
|
|
|
261
285
|
if(fadedPrimary(view.css))warnings.push('Wording: the CSS gives .sm-primary the page\'s background, so the main button blends in. Keep the kit\'s solid ink button.');
|
|
262
286
|
if(hiddenPick(view.css))warnings.push('Wording: the CSS restyles the picked choice (.sm-choices label:has(input:checked)), so the pick no longer reads at a glance. Keep the kit\'s ink fill and its text colour.');
|
|
263
287
|
const kept=DEVICE_STATE.exec(primary.replace(/<[^<>]*>/g,' ')+'\n'+quotedText(String(view.javascript||'')));
|
|
288
|
+
const loose=looseFields(primary);
|
|
289
|
+
if(loose>=3)warnings.push(`Wording: ${loose} settings sit loose on the page. Put what the person sets in one <div class="sm-fields"> (or .sm-settings for short ones): the kit gives the group its own surface, so the inputs, the settings and the result read apart.`);
|
|
264
290
|
if(kept)warnings.push(`Wording: the screen says where a file is kept ("${kept[0]}"). Show a picture or clip that cannot show here as a quiet <span class="sm-media-off"> tile with no words; StillMade says why once, as a notification.`);
|
|
265
291
|
return warnings;
|
|
266
292
|
}
|
|
@@ -3,12 +3,12 @@ import {EXAMPLE_PROVENANCE} from './example-license.js';
|
|
|
3
3
|
// the search; the Block names only what to read and what to do with it. Both
|
|
4
4
|
// are paid with StillMade credits at the app's research prices.
|
|
5
5
|
export const webFetch={
|
|
6
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.summarize-page',version:'1.0.0',name:'Summarize a page',description:'Read a public web page and summarize it in a few production-ready bullet points.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{url:{type:'text',primary:true,semantic:'page_url',required:true,description:'Public http or https page to read.'}},outputs:{summary:{type:'text',primary:true,semantic:'page_summary',description:'Summary of the page.'}},permissions:{project:[],capabilities:['web.fetch'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'url'}]},
|
|
6
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.summarize-page',action:{label:'Summarize page',running:'Summarizing page…'},version:'1.0.0',name:'Summarize a page',description:'Read a public web page and summarize it in a few production-ready bullet points.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{url:{type:'text',primary:true,semantic:'page_url',required:true,description:'Public http or https page to read.'}},outputs:{summary:{type:'text',primary:true,semantic:'page_summary',description:'Summary of the page.'}},permissions:{project:[],capabilities:['web.fetch'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'url'}]},
|
|
7
7
|
capability:{schemaVersion:1,operation:'web.fetch',url:{$input:'url'},instruction:'Summarize the page in five short bullet points a video producer can use. Quote numbers exactly.',output:'summary'},
|
|
8
8
|
tests:[{name:'Read a public page',input:{url:'https://en.wikipedia.org/wiki/Tide'},expectations:{summary:{kind:'text',minLength:20,maxLength:4000}}}],
|
|
9
9
|
};
|
|
10
10
|
export const webResearch={
|
|
11
|
-
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.research-topic',version:'1.0.0',name:'Research a topic',description:'Search the web for a topic and return a cited summary.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{topic:{type:'text',primary:true,semantic:'research_topic',required:true,description:'What to research.'}},outputs:{findings:{type:'json',primary:true,semantic:'research_findings',description:'Summary and the sources it came from.'}},permissions:{project:[],capabilities:['web.research'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'topic'}]},
|
|
11
|
+
manifest:{schemaVersion:1,sdkVersion:'0.1.0',id:'example.research-topic',action:{label:'Research topic',running:'Researching topic…'},version:'1.0.0',name:'Research a topic',description:'Search the web for a topic and return a cited summary.',kind:'task',runtime:'capability',entry:'src/capability.json',license:'MIT',provenance:EXAMPLE_PROVENANCE,inputs:{topic:{type:'text',primary:true,semantic:'research_topic',required:true,description:'What to research.'}},outputs:{findings:{type:'json',primary:true,semantic:'research_findings',description:'Summary and the sources it came from.'}},permissions:{project:[],capabilities:['web.research'],network:[],filesystem:[],secrets:[]},ui:[{control:'text',port:'topic'}]},
|
|
12
12
|
capability:{schemaVersion:1,operation:'web.research',topic:{$input:'topic'},output:'findings'},
|
|
13
13
|
tests:[{name:'Tides',input:{topic:'How tides work'},expectations:{findings:{kind:'json',includes:['summary']}}}],
|
|
14
14
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
The shape of a Block page, top to bottom:
|
|
8
8
|
.sm-block one workspace, no cards inside cards
|
|
9
|
+
.sm-fields what the person sets, as one group on its own surface
|
|
9
10
|
.sm-stage the thing being made (preview, media, result); biggest area
|
|
10
11
|
.sm-settings settings side by side; .sm-choices[data-segmented] for short ones
|
|
11
12
|
.sm-list many items, one line each; an item opens in place
|
|
@@ -88,6 +89,10 @@
|
|
|
88
89
|
: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}
|
|
89
90
|
:where(.sm-field) :where(textarea){min-height:96px;resize:vertical}
|
|
90
91
|
:where(.sm-fields){display:grid;gap:var(--s-4,16px);grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr))}
|
|
92
|
+
/* Sections stand apart (owner, November 3, 2026: "make sure it's more separated"). The group of settings on a Block's
|
|
93
|
+
page sits on its own surface, the same card StillMade's Step page gives its settings, so what goes in, the settings
|
|
94
|
+
and the result read as separate parts. Only a top-level group: never a card inside a card. */
|
|
95
|
+
:where(.sm-block) > :where(.sm-fields,.sm-settings){padding:var(--s-3,12px) var(--s-4,16px);border:1px solid var(--border);border-radius:var(--r-3,8px);background:var(--bg-secondary)}
|
|
91
96
|
/* Compact (October 19, 2026): a short field is one row at every width. On a phone or in a
|
|
92
97
|
narrow cell the label takes its own width and the control the rest; when the two do not
|
|
93
98
|
fit, the control wraps under its label. */
|