stillmade-sdk 0.1.40 → 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 +6 -2
- package/FRAME_VIEWS.md +41 -0
- package/INTERFACE_DESIGN.md +100 -8
- package/MOBILE_UI.md +3 -1
- package/PASTE_TO_LLM.md +18 -13
- package/SDK.md +200 -37
- package/UI_AESTHETIC.md +93 -4
- 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 +208 -37
- 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 +24 -2
- package/packages/block-platform/canvas.js +17 -0
- package/packages/block-platform/catalog.js +21 -6
- package/packages/block-platform/host-release-manifests.js +95 -0
- package/packages/block-platform/native-host-inventory.js +29 -4
- package/packages/block-platform/node-defaults.js +1 -0
- package/packages/block-platform/project-context.js +1 -1
- package/packages/block-platform/script-handoff.js +1 -1
- package/packages/block-platform/step-groups.js +3 -1
- package/packages/block-sdk/audio-generation-example.js +2 -2
- package/packages/block-sdk/builder-contract.js +15 -4
- package/packages/block-sdk/capability-example.js +1 -1
- package/packages/block-sdk/contracts.js +3 -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/index.d.ts +2 -2
- 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 +119 -18
- package/packages/block-sdk/web-example.js +2 -2
- package/packages/block-sdk/workspace-navigation.js +6 -2
- package/packages/design-system/block-kit.css +33 -6
- package/packages/design-system/block-kit.html +14 -4
- package/packages/design-system/theme.css +6 -5
|
@@ -11,7 +11,7 @@ export const BLOCK_KIT_CLASSES=Object.freeze({
|
|
|
11
11
|
'sm-block':'The one workspace. From 860px wide, a direct sm-stage child moves to the right and the controls keep a 300–380px column on the left; data-layout="stack" keeps one column, "split" puts the stage first at 1.6:1.',
|
|
12
12
|
'sm-stage':'The thing being made: preview, media or result. The biggest area; data-ratio="16:9|9:16|1:1".',
|
|
13
13
|
'sm-empty':'What an empty stage shows: an .sm-glyph, at most five words and one action.',
|
|
14
|
-
'sm-choices':'A fieldset of radio labels shown as pictures (img, .sm-swatch or .sm-glyph) and one or two words; replaces word dropdowns. data-compact for short text chips, data-segmented for one connected row (Low / Med / High). On a phone chips and segments scroll sideways.',
|
|
14
|
+
'sm-choices':'A fieldset of radio labels shown as pictures (img, .sm-swatch or .sm-glyph) and one or two words; replaces word dropdowns. data-compact for short text chips, data-segmented for one connected row (Low / Med / High). The pick is filled with the kit\'s ink; a disabled fieldset keeps its pick and fades the rest. On a phone chips and segments scroll sideways.',
|
|
15
15
|
'sm-field':'One short setting on one row, its label beside the control; on a phone the control wraps under the label only when both do not fit. A textarea, picture choices, file tiles or a list keep the label above. Group several in .sm-fields.',
|
|
16
16
|
'sm-settings':'Short settings side by side, each label beside its control (a narrow cell wraps the control under its label); .sm-choices[data-segmented] inside for three or four short options.',
|
|
17
17
|
'sm-list':'Many items one line each (thumbnail, one line of text, a short fact); an item opens in place with <details>. data-peek shows three on a phone.',
|
|
@@ -21,16 +21,19 @@ export const BLOCK_KIT_CLASSES=Object.freeze({
|
|
|
21
21
|
'sm-more':'A <details> for secondary settings; starts closed.',
|
|
22
22
|
'sm-hint':'A <details> info toggle for an explanation that might help; starts closed.',
|
|
23
23
|
'sm-gallery':'A grid of results or media. data-size="small" for input files: more tiles per row as the screen widens, never bigger tiles.',
|
|
24
|
-
'sm-
|
|
24
|
+
'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.',
|
|
25
|
+
'sm-glyph':'A small icon drawn in currentColor: data-glyph="play|pause|plus|close|check|chevron|upload|image|image-off|video|audio|text|spark|info|warning|spinner".',
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
// The design principles for every Block interface, in priority order.
|
|
28
29
|
export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
|
|
29
30
|
'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.',
|
|
30
|
-
'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). 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. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
|
|
31
|
-
'One obvious next action
|
|
32
|
-
'The action button names what it does ("
|
|
31
|
+
'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). 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.',
|
|
32
|
+
'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.',
|
|
33
|
+
'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.',
|
|
33
34
|
'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.',
|
|
35
|
+
'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.',
|
|
36
|
+
'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.',
|
|
34
37
|
'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.',
|
|
35
38
|
'Strong defaults; secondary settings go in `sm-more`, closed.',
|
|
36
39
|
'Compact: the whole Block fits one laptop screen without scrolling. A short setting is one row, its label beside the control; no card around a single control.',
|
|
@@ -42,12 +45,16 @@ export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
|
|
|
42
45
|
'Status sits beside the action it describes (`sm-status`), in a few words.',
|
|
43
46
|
'Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them.',
|
|
44
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.',
|
|
45
|
-
'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.',
|
|
46
|
-
'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.',
|
|
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
|
+
'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.',
|
|
47
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.',
|
|
48
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`).',
|
|
49
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.',
|
|
50
53
|
'Text scene descriptions are planning notes, never a storyboard or an approved frame.',
|
|
54
|
+
'Say nothing about how the Block works: no notes on where it runs (this device, StillMade Desktop, the cloud), saving, syncing, recovery, accounts, credits or where a file is kept. StillMade tells the person what they need as one short notification, or nothing; never a sentence on the page, and never the same line under each item.',
|
|
55
|
+
'References fill your inputs: every drop area in StillMade offers "Use a reference", and the person\'s saved photos, videos and notes fill the Step with no typing. Media inputs fill by kind, so take `video` or `asset` wherever a clip works as well as a photo; text inputs fill by plain names and labels (`productName`, `website`, `brandColors`, `fonts`, `voice`, and `details` or `notes` for the reference\'s note). Never build your own reference picker or library upload.',
|
|
56
|
+
'Days and times are picked, never typed: a `date` input is a calendar, with a list of times when it is about a time (`postAt`, `startsAt`, a label with Time, When or Starts). In a custom view use `<input type="date">` or `type="datetime-local"`, never a text box for a date.',
|
|
57
|
+
'Posting is its own Step: sending a result to Instagram, TikTok, email or any app is a connector Step after the Block, worded by StillMade ("Post to Instagram"). A Block never posts from inside itself.',
|
|
51
58
|
]);
|
|
52
59
|
// The compact layout rules (October 18–19, 2026), for every Block interface,
|
|
53
60
|
// generated or custom. StillMade's in-app builders and the SDK guide both read them.
|
|
@@ -188,6 +195,105 @@ function narrowScreen(view){
|
|
|
188
195
|
return narrowest;
|
|
189
196
|
}
|
|
190
197
|
|
|
198
|
+
// Speak the job (owner, November 2, 2026): the defaults a new Block's screen follows unless the person or creator asks
|
|
199
|
+
// otherwise. Warnings only, here and as readiness "Wording" limitations; they never refuse a release.
|
|
200
|
+
const GENERIC_JOB_ACTIONS=/^(?:make it|make|create|create it|generate|generate it|process|process it|do it|build it|start it)$/i;
|
|
201
|
+
const VAGUE_PROGRESS=/\bprogress\s*\(\s*[^,()]*,\s*['"`](?:processing|loading|working|please wait|running)\b/i;
|
|
202
|
+
const HOW_IT_WORKS=/\b(?:runs? (?:on|in) (?:your|this) (?:device|browser|computer)|while stillmade is open|stillmade desktop|saved automatically|auto-?saved?|kept in recovery|in recovery|syncs? (?:with|to) (?:stillmade|the cloud))\b/i;
|
|
203
|
+
const DATE_WORD=/(?:^|[\s_-])(?:date|day|time|when|deadline|schedule|starts?|yyyy-mm-dd|mm\/dd\/yyyy|dd\/mm\/yyyy)(?:$|[\s_-])|[a-z](?:Date|Time|At)$/;
|
|
204
|
+
function typedDate(html){
|
|
205
|
+
const lower=html.toLowerCase();
|
|
206
|
+
for(let at=lower.indexOf('<input');at!==-1;at=lower.indexOf('<input',at+6)){
|
|
207
|
+
const end=lower.indexOf('>',at);if(end===-1)return false;
|
|
208
|
+
const tag=html.slice(at,end),type=/\btype\s*=\s*["']?([a-z-]+)/i.exec(tag)?.[1]?.toLowerCase()||'text';
|
|
209
|
+
if(type!=='text'&&type!=='search')continue;
|
|
210
|
+
const values=[...tag.matchAll(/\b(?:id|name|placeholder|aria-label|data-stillmade-input)\s*=\s*["']([^"']{1,80})["']/gi)].map(match=>match[1]);
|
|
211
|
+
if(values.some(value=>DATE_WORD.test(value)))return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function fadedPrimary(css){
|
|
216
|
+
for(const rule of String(css||'').split('}')){
|
|
217
|
+
const open=rule.indexOf('{');if(open===-1)continue;
|
|
218
|
+
if(/\.sm-primary\b/.test(rule.slice(0,open))&&/background(?:-color)?\s*:\s*(?:transparent|none|inherit|initial|unset|var\(--bg\b)/i.test(rule.slice(open)))return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// November 2, 2026 (owner, of a Step's Kind and Voice rows: "there is no separation and I cannot tell what is going on"):
|
|
223
|
+
// a pick the Block's CSS takes back to a transparent or page-coloured fill, or gives the kit's ink as its text colour (on
|
|
224
|
+
// the kit's ink fill), no longer reads. Only a rule whose subject is the picked label itself (`…:has(input:checked)`)
|
|
225
|
+
// counts: a span inside it may be transparent over the label's fill. The subject is read with string searches: a regular
|
|
226
|
+
// expression over `:has(` and many `:checked` with no `)` took seconds on a 224 KB view CSS.
|
|
227
|
+
const picksChecked=selector=>{const subject=selector.trim(),at=subject.lastIndexOf(':has(');if(at===-1||!subject.endsWith(')'))return false;const inner=subject.slice(at+5,-1);return !/[()]/.test(inner)&&inner.includes(':checked');};
|
|
228
|
+
function hiddenPick(css){
|
|
229
|
+
for(const rule of String(css||'').split('}')){
|
|
230
|
+
const open=rule.indexOf('{');if(open===-1)continue;
|
|
231
|
+
if(!rule.slice(0,open).split(',').some(selector=>/\.sm-choices\b/.test(selector)&&picksChecked(selector)))continue;
|
|
232
|
+
const body=rule.slice(open),fill=/(?:^|[;{\s])background(?:-color)?\s*:\s*([^;]*)/i.exec(body)?.[1];
|
|
233
|
+
if(fill!==undefined?/^\s*(?:transparent|none|inherit|initial|unset|var\(--bg\b)/i.test(fill):/(?:^|[;{\s])color\s*:\s*var\(--fg\)/i.test(body))return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
// The same day (owner, of four tiles each saying "Only on the device it was added from": "if it's a notification make it a
|
|
238
|
+
// notification"): where a file is kept is StillMade's to say, once. Read in the page's words and the strings its code shows.
|
|
239
|
+
const DEVICE_STATE=/\b(?:only|kept|stored|saved) on (?:the|this|that|another|your|a different) (?:device|computer|browser)\b|\bnot (?:available )?on this (?:device|computer|browser)\b|\bthe device it was added (?:on|from)\b/i;
|
|
240
|
+
// The text inside quotes, in one linear pass (regular expressions here could retry at every position on unterminated quotes).
|
|
241
|
+
function quotedText(javascript){
|
|
242
|
+
let strings='',quote=null;
|
|
243
|
+
for(let index=0;index<javascript.length;index++){
|
|
244
|
+
const char=javascript[index];
|
|
245
|
+
if(!quote){if(char==='"'||char==="'"||char==='`'){quote=char;strings+='\n';}continue;}
|
|
246
|
+
if(char==='\\'){index++;continue;}
|
|
247
|
+
if(char===quote)quote=null;else strings+=char;
|
|
248
|
+
}
|
|
249
|
+
return strings;
|
|
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
|
+
}
|
|
275
|
+
function jobWordWarnings(view,primary){
|
|
276
|
+
const warnings=[],lower=primary.toLowerCase();
|
|
277
|
+
for(let start=lower.indexOf('<button'),close=0;start!==-1;start=lower.indexOf('<button',close+9)){
|
|
278
|
+
const open=lower.indexOf('>',start);close=open===-1?-1:lower.indexOf('</button>',open);if(close===-1)break;
|
|
279
|
+
const label=primary.slice(open+1,close).replace(/<[^<>]*>/g,' ').replace(/\s+/g,' ').trim();
|
|
280
|
+
if(GENERIC_JOB_ACTIONS.test(label)){warnings.push(`Wording: a button says "${label}". Name what it does in the job's words ("Plan the tour", "Generate voiceover", "Make 6 clips"), unless the person asked for "${label}".`);break;}
|
|
281
|
+
}
|
|
282
|
+
if(VAGUE_PROGRESS.test(String(view.javascript||'')))warnings.push('Wording: progress says "Processing" or "Loading". Count the job\'s units: stillmade.progress(0.4, "Moving through the kitchen, 4 of 9 rooms").');
|
|
283
|
+
if(HOW_IT_WORKS.test(primary.replace(/<[^<>]*>/g,' ')))warnings.push('Wording: a line explains where the Block runs or how it saves. Drop it; StillMade tells the person what they need.');
|
|
284
|
+
if(typedDate(primary))warnings.push('Wording: a date or time is typed into a text box. Use <input type="date"> or type="datetime-local", or a date input in the manifest, which StillMade shows as a calendar.');
|
|
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.');
|
|
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.');
|
|
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.`);
|
|
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.`);
|
|
291
|
+
return warnings;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The view's wording warnings as plain notes, for readiness's "Wording" limitations. */
|
|
295
|
+
export const viewWordingNotes=view=>view?jobWordWarnings(view,String(view.html||'').replace(/<details\b[\s\S]*?<\/details>/gi,'')).map(warning=>warning.replace(/^Wording: /,'').replace(/^./,letter=>letter.toUpperCase())):[];
|
|
296
|
+
|
|
191
297
|
export function reviewViewDesign(view){
|
|
192
298
|
if(!view)return {passed:true,issues:[],warnings:[],principles:BLOCK_INTERFACE_PRINCIPLES};
|
|
193
299
|
const html=view.html||'',javascript=view.javascript||'',primary=html.replace(/<details\b[\s\S]*?<\/details>/gi,'');
|
|
@@ -210,7 +316,11 @@ export function reviewViewDesign(view){
|
|
|
210
316
|
if(rareTool)warnings.push(`"${rareTool.slice(0,40)}" is its own section. Rare tools (update, schedule, history, reload) are named icon buttons beside the action (.sm-icon in .sm-actions), never sections under the work.`);
|
|
211
317
|
const rows=topLevelRows(primary);
|
|
212
318
|
if(rows>COMPACT_ROW_LIMIT)warnings.push(`The workspace has ${rows} visible rows besides the stage and may not fit one laptop screen (an estimate: this check counts rows and cannot measure height). Put short settings side by side in .sm-settings and secondary ones in a closed .sm-more.`);
|
|
213
|
-
|
|
319
|
+
warnings.push(...jobWordWarnings(view,primary));
|
|
320
|
+
// November 2, 2026: an app that lays itself out (data-stillmade-layout="own", exactly as the frame's selector reads it)
|
|
321
|
+
// chose its own width. Whether StillMade centers a narrow screen is measured in the Step (never html or body, never over
|
|
322
|
+
// a margin the view sets), so the warning promises nothing about it.
|
|
323
|
+
const narrow=[...html.matchAll(/\sdata-stillmade-layout\s*=\s*(["']?)([^"'\s>]*)\1/gi)].some(match=>match[2]==='own')?null:narrowScreen(view);
|
|
214
324
|
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.`);
|
|
215
325
|
warnings.push(...blockKitProblems(view).map(problem=>problem.message));
|
|
216
326
|
return {passed:issues.length===0,issues,warnings,principles:BLOCK_INTERFACE_PRINCIPLES};
|
|
@@ -236,16 +346,7 @@ const GENERIC_ACTIONS=/^(?:run|run step|run it|submit|go|start|ok|review cost|re
|
|
|
236
346
|
export const BLOCK_KIT_READINESS_RULES=Object.freeze(['action','price','heading']);
|
|
237
347
|
export function blockKitProblems(view){
|
|
238
348
|
if(!view)return [];
|
|
239
|
-
const problems=[],html=String(view.html||''),
|
|
240
|
-
// One linear pass over the code: the text inside quotes (regular expressions here
|
|
241
|
-
// could retry at every position on unterminated quotes).
|
|
242
|
-
let strings='',quote=null;
|
|
243
|
-
for(let index=0;index<javascript.length;index++){
|
|
244
|
-
const char=javascript[index];
|
|
245
|
-
if(!quote){if(char==='"'||char==="'"||char==='`'){quote=char;strings+='\n';}continue;}
|
|
246
|
-
if(char==='\\'){index++;continue;}
|
|
247
|
-
if(char===quote)quote=null;else strings+=char;
|
|
248
|
-
}
|
|
349
|
+
const problems=[],html=String(view.html||''),strings=quotedText(String(view.javascript||'')),original=view.appearance==='original';
|
|
249
350
|
const lower=html.toLowerCase();
|
|
250
351
|
// Each character is read a bounded number of times: the next search starts after this button.
|
|
251
352
|
for(let start=lower.indexOf('<button'),close=0;start!==-1;start=lower.indexOf('<button',close+9)){
|
|
@@ -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
|
};
|
|
@@ -4,9 +4,13 @@ export function createWorkspaceNavigationRegistry(discovered){
|
|
|
4
4
|
for(const namespace of Object.values(discovered)){
|
|
5
5
|
const source=namespace.workspaceNavigation;safeDocument(source);const d=structuredClone(source);
|
|
6
6
|
assert(plain(d)&&d.schemaVersion===1&&typeof d.id==='string'&&ID.test(d.id)&&Array.isArray(d.versions)&&d.versions.length>0&&d.versions.every(v=>typeof v==='string'&&VERSION.test(v))&&typeof d.phase==='string'&&typeof d.kind==='string','WORKSPACE_NAVIGATION','Invalid Block navigation declaration');
|
|
7
|
-
|
|
7
|
+
const validHost=host=>plain(host)&&Object.keys(host).every(key=>['canvasNode','ports','editorTab','exportVideo','exportKind','inspectClip'].includes(key))&&['canvasNode','editorTab','exportKind'].every(key=>host[key]===undefined||(typeof host[key]==='string'&&host[key].length>0&&host[key].length<=100))&&['exportVideo','inspectClip'].every(key=>host[key]===undefined||typeof host[key]==='boolean')&&(host.ports===undefined||(plain(host.ports)&&Object.values(host.ports).every(value=>typeof value==='string'&&value.length>0&&value.length<=100)));
|
|
8
|
+
if(d.host!==undefined)assert(validHost(d.host),'WORKSPACE_NAVIGATION','Invalid trusted host mapping');
|
|
9
|
+
// A later release can carry its own host mapping (November 2, 2026: Voiceover and Image Panels 1.2.0
|
|
10
|
+
// take wires through a Canvas port node) while earlier pins keep the declaration they shipped with.
|
|
11
|
+
if(d.releaseHosts!==undefined)assert(plain(d.releaseHosts)&&Object.entries(d.releaseHosts).every(([version,host])=>d.versions.includes(version)&&validHost(host)),'WORKSPACE_NAVIGATION','Invalid release host mapping');
|
|
8
12
|
assert(!definitions.has(d.id),'WORKSPACE_NAVIGATION','Duplicate Block navigation declaration');definitions.set(d.id,d);
|
|
9
|
-
for(const version of d.versions){const key=d.id+'@'+version;assert(!entries.has(key),'WORKSPACE_NAVIGATION','Duplicate Block navigation pin');entries.set(key,d);}
|
|
13
|
+
for(const version of d.versions){const key=d.id+'@'+version;assert(!entries.has(key),'WORKSPACE_NAVIGATION','Duplicate Block navigation pin');const release=d.releaseHosts?.[version];entries.set(key,release?Object.freeze({...d,host:release}):d);}
|
|
10
14
|
all.push(d);
|
|
11
15
|
}
|
|
12
16
|
const resolve=step=>typeof step?.blockId==='string'&&typeof step?.version==='string'?entries.get(step.blockId+'@'+step.version)||null:null;
|
|
@@ -6,10 +6,11 @@
|
|
|
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
|
|
12
|
-
.sm-choices picks shown as pictures or icons, not word lists
|
|
13
|
+
.sm-choices picks shown as pictures or icons, not word lists; the pick is ink
|
|
13
14
|
.sm-shape a frame shape at its real ratio (style="--r:9/16")
|
|
14
15
|
.sm-field the few inputs a person actually needs
|
|
15
16
|
.sm-actions one .sm-primary, optional .sm-quiet / .sm-icon
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
.sm-hint an info toggle for words that might help, collapsed
|
|
19
20
|
.sm-empty what an empty stage shows: a glyph, a few words, one action
|
|
20
21
|
.sm-glyph icons drawn in CSS with currentColor (no SVG or url needed)
|
|
22
|
+
.sm-media-off a picture or clip that cannot show here: a quiet tile, no words
|
|
21
23
|
|
|
22
24
|
The reference page is packages/design-system/block-kit.html. Keep this file
|
|
23
25
|
free of url(), @import and @font-face: Block views refuse to load resources. */
|
|
@@ -68,8 +70,9 @@
|
|
|
68
70
|
:where(.sm-actions) > :where(.sm-status){margin-inline-start:var(--s-2,8px)}
|
|
69
71
|
:where(.sm-primary,.sm-quiet){display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2,8px);min-height:var(--touch-target,44px);padding:10px 20px;border-radius:var(--r-pill,999px);font:inherit;font-weight:500;cursor:pointer;max-width:100%;text-align:center;overflow-wrap:anywhere}
|
|
70
72
|
:where(.sm-primary){border:1px solid var(--fg);background:var(--fg);color:var(--bg)}
|
|
71
|
-
:
|
|
72
|
-
:where(.sm-quiet):
|
|
73
|
+
/* Every button you act on stands out from the page: white with a strong edge (November 2, 2026). */
|
|
74
|
+
:where(.sm-quiet){border:1px solid var(--border-strong);background:var(--bg-elev-1);color:var(--fg)}
|
|
75
|
+
:where(.sm-quiet):hover{background:var(--bg-elev-2)}
|
|
73
76
|
:where(.sm-icon){display:inline-grid;place-items:center;min-width:var(--touch-target,44px);min-height:var(--touch-target,44px);padding:0;border:0;border-radius:var(--r-2,4px);background:transparent;color:var(--fg-muted);cursor:pointer}
|
|
74
77
|
:where(.sm-icon):hover{color:var(--fg);background:var(--hover-wash)}
|
|
75
78
|
:where(.sm-icon[aria-pressed="true"]){color:var(--fg);background:var(--press-wash)}
|
|
@@ -86,6 +89,10 @@
|
|
|
86
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}
|
|
87
90
|
:where(.sm-field) :where(textarea){min-height:96px;resize:vertical}
|
|
88
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)}
|
|
89
96
|
/* Compact (October 19, 2026): a short field is one row at every width. On a phone or in a
|
|
90
97
|
narrow cell the label takes its own width and the control the rest; when the two do not
|
|
91
98
|
fit, the control wraps under its label. */
|
|
@@ -107,10 +114,20 @@
|
|
|
107
114
|
:where(.sm-choices[data-size="wide"]){--sm-tile:160px}
|
|
108
115
|
:where(.sm-choices) > :where(legend){padding:0;margin-bottom:6px;font-size:var(--fs-body-s,.875rem);color:var(--fg-muted)}
|
|
109
116
|
:where(.sm-choices) > :where(label){position:relative;display:grid;justify-items:center;align-content:start;gap:6px;padding:var(--s-2,8px);border:1px solid var(--border-tok);border-radius:var(--r-3,8px);background:var(--bg-elev-1);color:var(--fg-muted);font-size:max(12px,var(--fs-caption,.8125rem));text-align:center;cursor:pointer}
|
|
110
|
-
:where(.sm-choices) > :where(label):hover{border-color:var(--border-strong);color:var(--fg)}
|
|
117
|
+
:where(.sm-choices) > :where(label:not(:has(input:checked,input:disabled))):hover{border-color:var(--border-strong);color:var(--fg)}
|
|
111
118
|
:where(.sm-choices) :where(input[type="radio"],input[type="checkbox"]){position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
|
|
112
|
-
|
|
119
|
+
/* The pick is the app's ink inversion, as in every segmented control in StillMade (owner, November 2, 2026, of a Step's
|
|
120
|
+
Kind and Voice rows: "there is like no separation and I cannot tell what is going on"). An outline alone read as a
|
|
121
|
+
slightly brighter border in Dark. Glyphs and shapes draw in currentColor, so they invert with the words. The muted
|
|
122
|
+
tokens invert too: in Light and Dark they are the ink at partial alpha, so a word inside the pick drawn in them (the
|
|
123
|
+
released Narration views' `#voice small{color:var(--fg-subtle)}`, "Onyx deep") vanished on the ink fill whatever its
|
|
124
|
+
selector's weight. The --text-* aliases were resolved at :root, so they are restated here. */
|
|
125
|
+
:where(.sm-choices) > :where(label:has(input:checked)){border-color:var(--fg);background:var(--fg);color:var(--bg);box-shadow:none;--fg-muted:color-mix(in srgb,var(--bg) 78%,transparent);--fg-subtle:color-mix(in srgb,var(--bg) 64%,transparent);--text-secondary:var(--fg-muted);--text-muted:var(--fg-subtle)}
|
|
113
126
|
:where(.sm-choices) > :where(label:has(input:focus-visible)){outline:2px solid var(--signal);outline-offset:2px}
|
|
127
|
+
/* A locked row (a disabled fieldset, a view-only person) keeps its pick readable and fades the rest; nothing in it
|
|
128
|
+
reacts to the pointer. Inputs in a disabled fieldset are :disabled themselves, so one rule covers both. */
|
|
129
|
+
:where(.sm-choices) > :where(label:has(input:disabled)),:where(.sm-choices) :where(input:disabled){cursor:default}
|
|
130
|
+
:where(.sm-choices) > :where(label:has(input:disabled):not(:has(input:checked))){opacity:.45}
|
|
114
131
|
:where(.sm-choices) :where(img,video,canvas,.sm-swatch){display:block;width:100%;aspect-ratio:var(--sm-tile-ratio,1);object-fit:cover;border-radius:var(--r-2,4px);background:var(--bg-inset)}
|
|
115
132
|
:where(.sm-choices) :where(.sm-glyph){--sm-glyph-size:28px;margin:10px 0 4px}
|
|
116
133
|
:where(.sm-choices[data-ratio="16:9"]){--sm-tile-ratio:16/9}
|
|
@@ -130,7 +147,8 @@
|
|
|
130
147
|
:where(.sm-choices[data-segmented]) > :where(label + label){margin-inline-start:-1px}
|
|
131
148
|
:where(.sm-choices[data-segmented]) > :where(label:first-of-type){border-start-start-radius:var(--r-pill,999px);border-end-start-radius:var(--r-pill,999px)}
|
|
132
149
|
:where(.sm-choices[data-segmented]) > :where(label:last-of-type){border-start-end-radius:var(--r-pill,999px);border-end-end-radius:var(--r-pill,999px)}
|
|
133
|
-
|
|
150
|
+
/* The picked segment keeps the ink fill above it; z-index keeps its edge over the next segment's -1px overlap. */
|
|
151
|
+
:where(.sm-choices[data-segmented]) > :where(label:has(input:checked)){position:relative;z-index:1}
|
|
134
152
|
:where(.sm-choices[data-segmented]) :where(.sm-glyph){--sm-glyph-size:16px;margin:0}
|
|
135
153
|
/* Settings side by side: a grid of short settings, each label beside its control. A cell
|
|
136
154
|
too narrow for both wraps the control under its label; long inputs keep the label above. */
|
|
@@ -174,6 +192,13 @@
|
|
|
174
192
|
:where(.sm-gallery) :where([aria-selected="true"],.is-selected){box-shadow:inset 0 0 0 2px var(--fg)}
|
|
175
193
|
/* Small tiles: more per row as the screen widens, never bigger tiles. */
|
|
176
194
|
:where(.sm-gallery[data-size="small"]){--sm-tile:92px;--sm-tile-ratio:3/2}
|
|
195
|
+
/* Media that cannot show here (kept on another device, or it did not open) keeps its place and its shape: a quiet tile
|
|
196
|
+
with the image-off glyph and no words (owner, November 2, 2026: four tiles each saying "Only on the device it was added
|
|
197
|
+
from"). The reason goes in its aria-label and title; StillMade says it once, as a notification.
|
|
198
|
+
<span class="sm-media-off" role="img" aria-label="On another device" title="On another device" style="--sm-photo-ratio:3/2"><span class="sm-glyph" data-glyph="image-off" aria-hidden="true"></span></span>
|
|
199
|
+
Its edge separates it from a stage or gallery tile of the same colour; in a gallery it takes the tile's shape. */
|
|
200
|
+
:where(.sm-media-off){display:inline-grid;place-items:center;box-sizing:border-box;width:min(100%,320px);aspect-ratio:var(--sm-photo-ratio,4/3);border:1px solid var(--border-tok);border-radius:var(--r-2,4px);background:var(--bg-inset);color:var(--fg-subtle,var(--fg-muted));vertical-align:top}
|
|
201
|
+
:where(.sm-gallery) :where(.sm-media-off){display:grid;width:100%;aspect-ratio:var(--sm-tile-ratio,1);border-radius:inherit}
|
|
177
202
|
|
|
178
203
|
/* ---------- Status: a glyph and a few words, next to the action ---------- */
|
|
179
204
|
:where(.sm-status){display:inline-flex;align-items:center;gap:var(--s-2,8px);min-height:24px;font-size:var(--fs-body-s,.875rem);color:var(--fg-muted)}
|
|
@@ -229,6 +254,8 @@
|
|
|
229
254
|
: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%)}
|
|
230
255
|
:where(.sm-glyph[data-glyph="image"])::before{inset:12%;border:2px solid currentColor;border-radius:3px}
|
|
231
256
|
: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%)}
|
|
257
|
+
: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%)}
|
|
258
|
+
:where(.sm-glyph[data-glyph="image-off"])::after{left:50%;top:-10%;bottom:-10%;width:2px;margin-left:-1px;background:currentColor;transform:rotate(-45deg)}
|
|
232
259
|
:where(.sm-glyph[data-glyph="video"])::before{left:8%;right:28%;top:22%;bottom:22%;border:2px solid currentColor;border-radius:3px}
|
|
233
260
|
:where(.sm-glyph[data-glyph="video"])::after{right:8%;top:30%;bottom:30%;width:20%;background:currentColor;clip-path:polygon(0 35%,100% 0,100% 100%,0 65%)}
|
|
234
261
|
:where(.sm-glyph[data-glyph="audio"])::before{inset:15% 12%;background:linear-gradient(currentColor,currentColor) 0 60%/2px 40% no-repeat,linear-gradient(currentColor,currentColor) 33% 50%/2px 80% no-repeat,linear-gradient(currentColor,currentColor) 66% 50%/2px 100% no-repeat,linear-gradient(currentColor,currentColor) 100% 60%/2px 50% no-repeat}
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
</div>
|
|
85
85
|
|
|
86
86
|
<div class="sm-actions">
|
|
87
|
-
<button type="button" class="sm-primary" id="run">Make
|
|
87
|
+
<button type="button" class="sm-primary" id="run">Make the clip</button>
|
|
88
88
|
<p class="sm-status" id="status" role="status"></p>
|
|
89
89
|
</div>
|
|
90
90
|
|
|
@@ -162,7 +162,8 @@
|
|
|
162
162
|
</div>
|
|
163
163
|
</section>
|
|
164
164
|
|
|
165
|
-
<!-- ===== 4. Choices as pictures instead of word lists.
|
|
165
|
+
<!-- ===== 4. Choices as pictures instead of word lists. The pick is the ink inversion, so it reads at a
|
|
166
|
+
glance; a locked row (disabled, or a view-only person) keeps its pick and fades the rest. ===== -->
|
|
166
167
|
<section>
|
|
167
168
|
<h2>Choices as pictures</h2>
|
|
168
169
|
<div class="ref-frame" style="display:grid;gap:24px">
|
|
@@ -179,6 +180,13 @@
|
|
|
179
180
|
<label><input type="radio" name="shape"><span>9:16</span></label>
|
|
180
181
|
<label><input type="radio" name="shape"><span>1:1</span></label>
|
|
181
182
|
</fieldset>
|
|
183
|
+
<!-- A quiet word in a tile may use the muted tokens: inside the pick they invert with the fill. -->
|
|
184
|
+
<fieldset class="sm-choices" data-compact disabled>
|
|
185
|
+
<legend>Voice (view only)</legend>
|
|
186
|
+
<label><input type="radio" name="voice"><span>Nova</span><small style="color:var(--fg-subtle)">bright</small></label>
|
|
187
|
+
<label><input type="radio" name="voice" checked><span>Onyx</span><small style="color:var(--fg-subtle)">deep</small></label>
|
|
188
|
+
<label><input type="radio" name="voice"><span>Sage</span><small style="color:var(--fg-subtle)">calm</small></label>
|
|
189
|
+
</fieldset>
|
|
182
190
|
</div>
|
|
183
191
|
</section>
|
|
184
192
|
|
|
@@ -194,6 +202,7 @@
|
|
|
194
202
|
<span class="sm-glyph" data-glyph="chevron" title="chevron"></span>
|
|
195
203
|
<span class="sm-glyph" data-glyph="upload" title="upload"></span>
|
|
196
204
|
<span class="sm-glyph" data-glyph="image" title="image"></span>
|
|
205
|
+
<span class="sm-glyph" data-glyph="image-off" title="image-off"></span>
|
|
197
206
|
<span class="sm-glyph" data-glyph="video" title="video"></span>
|
|
198
207
|
<span class="sm-glyph" data-glyph="audio" title="audio"></span>
|
|
199
208
|
<span class="sm-glyph" data-glyph="text" title="text"></span>
|
|
@@ -204,7 +213,8 @@
|
|
|
204
213
|
</div>
|
|
205
214
|
</section>
|
|
206
215
|
|
|
207
|
-
<!-- ===== 6. Results: show the thing, not a description of it.
|
|
216
|
+
<!-- ===== 6. Results: show the thing, not a description of it. A picture that cannot show here is a
|
|
217
|
+
quiet tile with no words; StillMade says why once, as a notification. ===== -->
|
|
208
218
|
<section>
|
|
209
219
|
<h2>Gallery</h2>
|
|
210
220
|
<div class="ref-frame">
|
|
@@ -212,7 +222,7 @@
|
|
|
212
222
|
<li><span class="sm-swatch swatch-a" style="display:block;aspect-ratio:1"></span></li>
|
|
213
223
|
<li class="is-selected"><span class="sm-swatch swatch-b" style="display:block;aspect-ratio:1"></span></li>
|
|
214
224
|
<li><span class="sm-swatch swatch-c" style="display:block;aspect-ratio:1"></span></li>
|
|
215
|
-
<li><span class="sm-
|
|
225
|
+
<li><span class="sm-media-off" role="img" aria-label="On another device" title="On another device"><span class="sm-glyph" data-glyph="image-off" aria-hidden="true"></span></span></li>
|
|
216
226
|
</ul>
|
|
217
227
|
</div>
|
|
218
228
|
</section>
|
|
@@ -229,13 +229,14 @@
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
|
|
232
|
-
/* White — a truly white page
|
|
233
|
-
|
|
232
|
+
/* White — a truly white page; the sidebar stays black (owner, October 20, 2026: the gray page read off, not white).
|
|
233
|
+
Panels sit one tone below the page and insets one more, so each layer differs from what it sits on, as cream does
|
|
234
|
+
in Light (owner, November 1, 2026: White read flat, with page, panels and covers all #FFFFFF). */
|
|
234
235
|
[data-mode="white"] {
|
|
235
236
|
color-scheme: light;
|
|
236
|
-
--bg: #FFFFFF; --bg-elev-1: #
|
|
237
|
+
--bg: #FFFFFF; --bg-elev-1: #F4F4F1; --bg-elev-2: #EAEAE6; --bg-inset: #EEEEEA;
|
|
237
238
|
--fg: #0E0E0E; --fg-muted: #5C5C5C; --fg-subtle: #767676; --fg-on-accent: #FFFFFF;
|
|
238
|
-
--border-tok: #
|
|
239
|
+
--border-tok: #E1E1DD; --border-strong: #C4C4BF; --divider: #E7E7E3;
|
|
239
240
|
--hover-wash: rgba(0,0,0,.05); --press-wash: rgba(0,0,0,.09);
|
|
240
241
|
--selection-bg: #0E0E0E; --selection-fg: #FFFFFF;
|
|
241
242
|
--signal: #8E6B46; --signal-dim: rgba(142,107,70,.10); --signal-glow: rgba(142,107,70,.22);
|
|
@@ -243,7 +244,7 @@
|
|
|
243
244
|
--scrim-top: linear-gradient(to bottom,rgba(255,255,255,.9),rgba(255,255,255,0));
|
|
244
245
|
--scrim-bottom: linear-gradient(to top,rgba(255,255,255,.9),rgba(255,255,255,0));
|
|
245
246
|
--bg-primary: var(--bg); --bg-secondary: var(--bg-elev-1); --bg-tertiary: var(--bg-elev-2);
|
|
246
|
-
--bg-elevated: var(--bg-elev-1); --bg-hover: #
|
|
247
|
+
--bg-elevated: var(--bg-elev-1); --bg-hover: #ECECE8;
|
|
247
248
|
--border: var(--border-tok); --border-light: var(--border-strong);
|
|
248
249
|
--accent: var(--fg); --accent-hover: #2A2A2A; --accent-dim: var(--hover-wash);
|
|
249
250
|
--accent-glow: rgba(0,0,0,.12); --accent-contrast: var(--bg);
|