synabun 2.2.0 → 2.2.1
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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/neural-interface/i18n/en.json +3 -0
- package/neural-interface/i18n/pt-BR.json +3 -0
- package/neural-interface/lib/assistant-brains/codex.js +6 -1
- package/neural-interface/lib/assistant-dispatch.js +4 -1
- package/neural-interface/lib/mcp-external-servers.js +27 -3
- package/neural-interface/lib/native-loop-providers.js +3 -2
- package/neural-interface/lib/opencode-native-boundary.js +155 -21
- package/neural-interface/lib/peer-identity.js +49 -5
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/assistant/asst-api.js +14 -2
- package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
- package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1 -1
- package/neural-interface/public/shared/assistant/asst-render.js +62 -7
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
- package/neural-interface/public/shared/assistant/asst-scenes.js +225 -79
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
- package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +24 -4
- package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
- package/neural-interface/public/shared/loading-teardown.js +34 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +1 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +10 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +88 -72
- package/neural-interface/public/shared/storage-retry.js +47 -0
- package/neural-interface/public/shared/storage.js +25 -4
- package/neural-interface/public/shared/styles.css +8 -1
- package/neural-interface/public/shared/synabun-mascot.js +27 -8
- package/neural-interface/public/shared/synabun-ticker.js +147 -12
- package/neural-interface/public/shared/ui-claude-panel.js +79 -61
- package/neural-interface/public/shared/ui-loading.js +5 -4
- package/neural-interface/server.js +87 -40
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Assistant — loops run only on screen
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
// The transcript's decorative loops (a run card's cameo, its stage's scene and
|
|
5
|
+
// sweep; a live rack's sweep, ticks and scene; a route line's sweep and icon)
|
|
6
|
+
// run only while their holder is on screen. One IntersectionObserver per
|
|
7
|
+
// renderer marks a holder that is not (scrolled out of its transcript, its tab
|
|
8
|
+
// hidden, its window minimised to nothing) with `data-offscreen`; the
|
|
9
|
+
// stylesheet pauses every animation under it (asst-styles.js), and the loop
|
|
10
|
+
// picks up where it stopped when the holder is back. Every frame the page
|
|
11
|
+
// draws restyles each running animation on the main thread, composited or
|
|
12
|
+
// not, so a loop nobody sees is never free; the observer is the browser's own,
|
|
13
|
+
// so watching costs nothing while the transcript scrolls.
|
|
14
|
+
//
|
|
15
|
+
// createMotionGate({ IntersectionObserver }?) → { watch(el), unwatch(el), disconnect(), size() }
|
|
16
|
+
//
|
|
17
|
+
// No observer (an old engine, a test without one): nothing is marked, every
|
|
18
|
+
// loop runs as before. DOM-light on purpose: tests/assistant-motion-gate.test.mjs
|
|
19
|
+
// drives it with a stand-in observer.
|
|
20
|
+
|
|
21
|
+
export const OFFSCREEN_ATTR = 'data-offscreen';
|
|
22
|
+
const PRUNE_EVERY = 32; // watched holders between two sweeps for ones that left the document
|
|
23
|
+
|
|
24
|
+
export function createMotionGate({ IntersectionObserver: IO = globalThis.IntersectionObserver } = {}) {
|
|
25
|
+
const watched = new Set();
|
|
26
|
+
let io = null; // undefined: no observer here
|
|
27
|
+
|
|
28
|
+
function mark(el, off) {
|
|
29
|
+
try {
|
|
30
|
+
if (off) { if (!el.hasAttribute(OFFSCREEN_ATTR)) el.setAttribute(OFFSCREEN_ATTR, ''); }
|
|
31
|
+
else if (el.hasAttribute(OFFSCREEN_ATTR)) el.removeAttribute(OFFSCREEN_ATTR);
|
|
32
|
+
} catch { /* not an element */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function observer() {
|
|
36
|
+
if (io !== null) return io;
|
|
37
|
+
if (typeof IO !== 'function') { io = undefined; return io; }
|
|
38
|
+
try {
|
|
39
|
+
io = new IO((entries) => {
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const el = entry.target;
|
|
42
|
+
if (!watched.has(el)) continue;
|
|
43
|
+
if (el.isConnected === false) { unwatch(el); continue; }
|
|
44
|
+
mark(el, !entry.isIntersecting);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} catch { io = undefined; }
|
|
48
|
+
return io;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Holders removed while already off screen get no report: drop them now and then. */
|
|
52
|
+
function prune() {
|
|
53
|
+
for (const el of Array.from(watched)) if (el.isConnected === false) unwatch(el);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function watch(el) {
|
|
57
|
+
if (!el || watched.has(el)) return false;
|
|
58
|
+
const o = observer();
|
|
59
|
+
if (!o) return false;
|
|
60
|
+
watched.add(el);
|
|
61
|
+
try { o.observe(el); } catch { watched.delete(el); return false; }
|
|
62
|
+
if (watched.size % PRUNE_EVERY === 0) prune();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function unwatch(el) {
|
|
67
|
+
if (!watched.delete(el)) return;
|
|
68
|
+
try { io?.unobserve(el); } catch { /* already gone */ }
|
|
69
|
+
mark(el, false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function disconnect() {
|
|
73
|
+
try { io?.disconnect(); } catch { /* already gone */ }
|
|
74
|
+
for (const el of watched) mark(el, false);
|
|
75
|
+
watched.clear();
|
|
76
|
+
if (io) io = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { watch, unwatch, disconnect, size: () => watched.size };
|
|
80
|
+
}
|
|
@@ -1118,7 +1118,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1118
1118
|
computerPanel.applyStatus(status);
|
|
1119
1119
|
applyFeatures();
|
|
1120
1120
|
})
|
|
1121
|
-
.catch(() => { st.features.computer = false; applyFeatures(); });
|
|
1121
|
+
.catch((err) => { st.features.computer = false; computerPanel.checkFailed?.(err); applyFeatures(); });
|
|
1122
1122
|
}
|
|
1123
1123
|
}
|
|
1124
1124
|
|
|
@@ -55,6 +55,8 @@ import { injectRackStyles } from './asst-rack-styles.js';
|
|
|
55
55
|
import { injectSceneStyles } from './asst-scene-styles.js';
|
|
56
56
|
import { activityScene, compactActivity, isLivePhase, runChip, sceneOfClass, sceneSvg, stagePhase } from './asst-scenes.js';
|
|
57
57
|
import { STAGE_TIMING, createWorkerStage } from './asst-run-stage.js';
|
|
58
|
+
import { createMotionGate } from './asst-motion-gate.js';
|
|
59
|
+
import { createStyleCache } from './asst-style-cache.js';
|
|
58
60
|
import { formatRunTokens } from './asst-usage.js';
|
|
59
61
|
import { sanitizeHtmlString, sanitizeInto } from './asst-sanitize.js';
|
|
60
62
|
import { renderStreamingMarkdown } from '../streaming-markdown.js';
|
|
@@ -574,6 +576,16 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
574
576
|
const stageWidths = new WeakMap(); // stage element → { rack, width }
|
|
575
577
|
const toRecut = new Set();
|
|
576
578
|
let recutTimer = 0;
|
|
579
|
+
// What the stylesheet decides about a bubble (--rk-cut, the line's white-space) changes only with a container
|
|
580
|
+
// query, so only with the stage's width: read once, forgotten when the width changes (here) or the fonts load.
|
|
581
|
+
const styleCache = createStyleCache();
|
|
582
|
+
// A web font that arrives changes every measured width: the next paint of each bubble measures again.
|
|
583
|
+
const onFontsLoaded = () => { styleCache.invalidate(); for (const rack of racks) rack.budgetMemo = null; };
|
|
584
|
+
const fontSet = typeof document !== 'undefined' ? document.fonts : null;
|
|
585
|
+
try { fontSet?.addEventListener?.('loadingdone', onFontsLoaded); } catch { /* no FontFaceSet events */ }
|
|
586
|
+
const stopFontWatch = () => { try { fontSet?.removeEventListener?.('loadingdone', onFontsLoaded); } catch { /* gone */ } };
|
|
587
|
+
// The transcript's loops run only while their holder is on screen (run cards, racks, route lines).
|
|
588
|
+
const motion = createMotionGate();
|
|
577
589
|
const stageResizes = typeof ResizeObserver === 'function' ? new ResizeObserver((entries) => {
|
|
578
590
|
for (const entry of entries) {
|
|
579
591
|
const seen = stageWidths.get(entry.target);
|
|
@@ -582,6 +594,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
582
594
|
if (seen.width === null) { seen.width = width; continue; } // the first report: the width it was cut at
|
|
583
595
|
if (width === seen.width) continue;
|
|
584
596
|
seen.width = width;
|
|
597
|
+
forgetBubbleStyle(seen.rack);
|
|
585
598
|
toRecut.add(seen.rack);
|
|
586
599
|
}
|
|
587
600
|
if (!toRecut.size) return;
|
|
@@ -1601,6 +1614,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
1601
1614
|
sr: { last: 0, timer: 0, pending: '' },
|
|
1602
1615
|
};
|
|
1603
1616
|
row._rack = rack;
|
|
1617
|
+
motion.watch(row); // its sweep, ticks and scene loop only while it is on screen
|
|
1604
1618
|
rack.receipt.btn.addEventListener('click', () => setRackOpen(rack, !rack.open));
|
|
1605
1619
|
row.querySelector('.asst-rack-view').addEventListener('click', (e) => {
|
|
1606
1620
|
const btn = e.target.closest('button[data-view]');
|
|
@@ -2079,6 +2093,22 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2079
2093
|
* head (subjectCut).
|
|
2080
2094
|
*/
|
|
2081
2095
|
function subjectBudget(rack, verb, cut = 'middle') {
|
|
2096
|
+
const s = rack.stage;
|
|
2097
|
+
// The same inputs give the same budget: the 1 Hz clock tick reuses it instead of measuring the stage again
|
|
2098
|
+
// (a measure on the frame's path forces style and layout of whatever the frame dirtied). The key holds every
|
|
2099
|
+
// input the measure reads: the stage's width (its ResizeObserver's), the scene beside the mascot, whether the
|
|
2100
|
+
// bubble shows, the verb, the clock's length (tabular digits: same length, same width), the running reserve, the cut.
|
|
2101
|
+
const seen = stageResizes ? stageWidths.get(s.el) : null;
|
|
2102
|
+
const key = seen && seen.width != null
|
|
2103
|
+
? `${seen.width}|${rack.sceneId}|${s.bubble.hidden ? 0 : 1}|${verb}|${s.meta.textContent.length}|${rack.bubble.step?.state === 'running' ? 1 : 0}|${cut}`
|
|
2104
|
+
: '';
|
|
2105
|
+
if (key && rack.budgetMemo?.key === key) return rack.budgetMemo.value;
|
|
2106
|
+
const value = measureSubjectBudget(rack, verb, cut);
|
|
2107
|
+
rack.budgetMemo = key ? { key, value } : null;
|
|
2108
|
+
return value;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function measureSubjectBudget(rack, verb, cut) {
|
|
2082
2112
|
const s = rack.stage;
|
|
2083
2113
|
const width = s.el.clientWidth;
|
|
2084
2114
|
if (!width) return 96;
|
|
@@ -2095,14 +2125,30 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2095
2125
|
// Two lines, less what wrapping at token boundaries wastes (a head cut is one tail: close to two full
|
|
2096
2126
|
// lines, a path's pieces wrapping at its separators); a guess on the generous side, since fitSubject
|
|
2097
2127
|
// cuts what the clamp would hide or a piece would push past the edge.
|
|
2098
|
-
const lines =
|
|
2128
|
+
const lines = bubbleStyle(rack, s.line, 'white-space') === 'nowrap' ? 1 : cut === 'head' ? 2 : 1.85;
|
|
2099
2129
|
const taken = Math.ceil((verbPx + 7 + (numbersPx ? numbersPx + 7 : 0)) / 7);
|
|
2100
2130
|
return Math.max(24, Math.min(240, Math.floor(perLine * lines) - taken - 1));
|
|
2101
2131
|
}
|
|
2102
2132
|
|
|
2103
|
-
/**
|
|
2133
|
+
/**
|
|
2134
|
+
* A computed style of the bubble, read once while the stage keeps its width (styleCache; the stage's
|
|
2135
|
+
* ResizeObserver forgets it). Without a ResizeObserver nothing would say when to forget: read it each time.
|
|
2136
|
+
*/
|
|
2137
|
+
function bubbleStyle(rack, node, prop) {
|
|
2138
|
+
if (stageResizes && stageWidths.has(rack.stage.el)) return styleCache.get(node, prop);
|
|
2139
|
+
try { return getComputedStyle(node).getPropertyValue(prop).trim(); } catch { return ''; }
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/** The stage's width changed (or the fonts did): its cut, its line's wrap and its budget are read again. */
|
|
2143
|
+
function forgetBubbleStyle(rack) {
|
|
2144
|
+
styleCache.invalidate(rack.stage.subject);
|
|
2145
|
+
styleCache.invalidate(rack.stage.line);
|
|
2146
|
+
rack.budgetMemo = null;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/** 'head' where the stylesheet says so (≤320 px: keep the tail), else 'middle'. Never read per frame: see bubbleStyle. */
|
|
2104
2150
|
function subjectCut(rack) {
|
|
2105
|
-
|
|
2151
|
+
return bubbleStyle(rack, rack.stage.subject, '--rk-cut') === 'head' ? 'head' : 'middle';
|
|
2106
2152
|
}
|
|
2107
2153
|
|
|
2108
2154
|
/**
|
|
@@ -2861,9 +2907,11 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2861
2907
|
}
|
|
2862
2908
|
|
|
2863
2909
|
// ── Rack upkeep ──
|
|
2910
|
+
// The 1 Hz beats run only while the transcript is on screen (a hidden tab, a closed panel): they paint clocks
|
|
2911
|
+
// and bubbles nobody would see. Back on screen, the ticker runs them at once.
|
|
2864
2912
|
function needTicker() {
|
|
2865
2913
|
if (unsubTick) return;
|
|
2866
|
-
try { unsubTick = subscribe(tickRacks, { fps: 1 }); } catch { unsubTick = null; }
|
|
2914
|
+
try { unsubTick = subscribe(tickRacks, { fps: 1, target: $msgs }); } catch { unsubTick = null; }
|
|
2867
2915
|
}
|
|
2868
2916
|
|
|
2869
2917
|
function stopTicker() {
|
|
@@ -2888,6 +2936,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2888
2936
|
const stage = rack.stage.el;
|
|
2889
2937
|
if (on && !stageWidths.has(stage)) {
|
|
2890
2938
|
stageWidths.set(stage, { rack, width: null });
|
|
2939
|
+
forgetBubbleStyle(rack); // read again from here on, while the observer watches the width
|
|
2891
2940
|
stageResizes.observe(stage);
|
|
2892
2941
|
} else if (!on && stageWidths.has(stage)) {
|
|
2893
2942
|
stageWidths.delete(stage);
|
|
@@ -2914,6 +2963,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2914
2963
|
|
|
2915
2964
|
function dropRack(rack) {
|
|
2916
2965
|
watchStageWidth(rack, false);
|
|
2966
|
+
motion.unwatch(rack.row);
|
|
2917
2967
|
clearTimeout(rack.stageTimer);
|
|
2918
2968
|
clearTimeout(rack.clearTimer);
|
|
2919
2969
|
clearTimeout(rack.sr.timer);
|
|
@@ -3178,6 +3228,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
3178
3228
|
runCards.set(run.runId, card);
|
|
3179
3229
|
const { wrap } = assistantRow(rootScope);
|
|
3180
3230
|
wrap.appendChild(card);
|
|
3231
|
+
motion.watch(card); // its cameo, stage, scene and sweep loop only while it is on screen
|
|
3181
3232
|
syncStage(run.runId, card, run);
|
|
3182
3233
|
return card;
|
|
3183
3234
|
}
|
|
@@ -3220,7 +3271,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
3220
3271
|
|
|
3221
3272
|
function needStageTicker() {
|
|
3222
3273
|
if (unsubStageTick) return;
|
|
3223
|
-
try { unsubStageTick = subscribe(tickStages, { fps: 1 }); } catch { unsubStageTick = null; }
|
|
3274
|
+
try { unsubStageTick = subscribe(tickStages, { fps: 1, target: $msgs }); } catch { unsubStageTick = null; }
|
|
3224
3275
|
}
|
|
3225
3276
|
|
|
3226
3277
|
function stopStageTicker() {
|
|
@@ -3399,6 +3450,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
3399
3450
|
const line = createRouteLine(route, routeLineOpts(phase));
|
|
3400
3451
|
routeLines.set(routeId, line);
|
|
3401
3452
|
append(line.el);
|
|
3453
|
+
motion.watch(line.el); // its sweep and icon loop only while it is on screen
|
|
3402
3454
|
syncRouteLive(routeId);
|
|
3403
3455
|
enterRow(line.el);
|
|
3404
3456
|
return line;
|
|
@@ -3409,6 +3461,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
3409
3461
|
if (!routeId || !line) return;
|
|
3410
3462
|
const fresh = routeLines.get(routeId) !== line;
|
|
3411
3463
|
routeLines.set(routeId, line);
|
|
3464
|
+
if (line.el) motion.watch(line.el);
|
|
3412
3465
|
syncRouteLive(routeId);
|
|
3413
3466
|
if (fresh && line.el?.isConnected) enterRow(line.el);
|
|
3414
3467
|
}
|
|
@@ -3430,7 +3483,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
3430
3483
|
|
|
3431
3484
|
function removeRouteLine(routeId) {
|
|
3432
3485
|
const line = routeLines.get(routeId);
|
|
3433
|
-
if (line) { line.el.remove(); routeLines.delete(routeId); }
|
|
3486
|
+
if (line) { motion.unwatch(line.el); line.el.remove(); routeLines.delete(routeId); }
|
|
3434
3487
|
}
|
|
3435
3488
|
|
|
3436
3489
|
// ── The automation trace ──
|
|
@@ -4094,6 +4147,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
4094
4147
|
runActivity.clear();
|
|
4095
4148
|
runCards.clear();
|
|
4096
4149
|
routeLines.clear();
|
|
4150
|
+
motion.disconnect(); // every holder it watched goes with the transcript
|
|
4151
|
+
styleCache.invalidate();
|
|
4097
4152
|
autoRows.clear();
|
|
4098
4153
|
turnFrames = [];
|
|
4099
4154
|
rootScope.stream = null;
|
|
@@ -4122,7 +4177,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
4122
4177
|
return {
|
|
4123
4178
|
el: $msgs,
|
|
4124
4179
|
clear,
|
|
4125
|
-
destroy: () => { clear(); stageResizes?.disconnect(); limitDock.destroy(); },
|
|
4180
|
+
destroy: () => { clear(); stageResizes?.disconnect(); limitDock.destroy(); stopFontWatch(); },
|
|
4126
4181
|
appendUser,
|
|
4127
4182
|
appendAssistantMarkdown,
|
|
4128
4183
|
appendStatus,
|
|
@@ -15,31 +15,52 @@
|
|
|
15
15
|
// - the agents tray's mini scenes and lanes (asst-agents-dock.js).
|
|
16
16
|
//
|
|
17
17
|
// Rules as the rack's: gold only means live; infinite loops pause while the
|
|
18
|
-
// UI is dragged (body.ui-interacting)
|
|
18
|
+
// UI is dragged (body.ui-interacting), off screen (data-offscreen on a holder,
|
|
19
|
+
// a closed side panel: asst-styles.js) and stop under prefers-reduced-motion,
|
|
19
20
|
// where every scene is its still frame and the sweep is not drawn. Tokens
|
|
20
21
|
// come from asst-styles.js, always with a fallback. No backdrop blur and no
|
|
21
|
-
// standing layer hints
|
|
22
|
+
// standing layer hints: a moving layer is promoted (will-change) only while
|
|
23
|
+
// its scene is live and on screen.
|
|
24
|
+
//
|
|
25
|
+
// A scene's moving parts are HTML layers (asst-scenes.js), so every motion is
|
|
26
|
+
// a box's transform or opacity, run by the compositor. Lengths are written in
|
|
27
|
+
// the scene's own units (the SVG's px) and turned into percentages of the box
|
|
28
|
+
// here (sc() below): a scene is drawn at several sizes, and a percentage of
|
|
29
|
+
// the box is the same distance in units at every one of them.
|
|
22
30
|
|
|
23
|
-
import { CODE_LINES, CODE_STEP, codeCaretX } from './asst-scenes.js';
|
|
31
|
+
import { CODE_LINES, CODE_STEP, CODE_WINDOW, FILM_MINI_WINDOW, FILM_WINDOW, SCENE_BOX, boxPct, codeCaretX } from './asst-scenes.js';
|
|
24
32
|
|
|
25
33
|
export const SCENE_STYLE_ID = 'asst-scene-styles';
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
const FULL = SCENE_BOX.full;
|
|
36
|
+
const MINI = SCENE_BOX.mini;
|
|
37
|
+
const win = ([, , w, h]) => ({ w, h });
|
|
38
|
+
|
|
39
|
+
/** A move of (x, y) scene units in a box: `translate(…%, …%)`, plus any scale or rotation after it. */
|
|
40
|
+
function sc(box, x, y, more = '') {
|
|
41
|
+
return `transform: translate(${boxPct(x, box.w)}, ${boxPct(y, box.h)})${more ? ` ${more}` : ''};`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The code scene's stepped scroll (one line per step, inside its window) and its caret, which lands on each new bottom line's end. */
|
|
28
45
|
function codeFrames() {
|
|
29
46
|
const n = CODE_LINES.length;
|
|
30
47
|
const at = (s) => Math.round((s * 100000) / n) / 1000;
|
|
31
48
|
const hold = (s) => Math.round(((s * 100) / n + (100 / n) * 0.72) * 1000) / 1000;
|
|
49
|
+
const view = win(CODE_WINDOW);
|
|
32
50
|
let scroll = '';
|
|
33
51
|
let caret = '';
|
|
34
52
|
for (let s = 0; s < n; s += 1) {
|
|
35
|
-
scroll += ` ${at(s)}%, ${hold(s)}% {
|
|
36
|
-
caret += ` ${at(s)}%, ${hold(s)}% {
|
|
53
|
+
scroll += ` ${at(s)}%, ${hold(s)}% { ${sc(view, 0, -CODE_STEP * s)} }`;
|
|
54
|
+
caret += ` ${at(s)}%, ${hold(s)}% { ${sc(FULL, codeCaretX(s), 0)} }`;
|
|
37
55
|
}
|
|
38
|
-
scroll += ` 100% {
|
|
39
|
-
caret += ` 100% {
|
|
56
|
+
scroll += ` 100% { ${sc(view, 0, -CODE_STEP * n)} }`;
|
|
57
|
+
caret += ` 100% { ${sc(FULL, codeCaretX(n), 0)} }`;
|
|
40
58
|
return `@keyframes asst-sc-code {${scroll} }\n @keyframes asst-sc-code-caret {${caret} }`;
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
/** The pointer scenes: rest on the button, travel to the page, back (units of their own box). */
|
|
62
|
+
const pointFrames = (name, box, [x0, y0], [x1, y1], more = '', [a, b, c, d] = [22, 46, 72, 96]) => `@keyframes ${name} { 0%, ${a}% { ${sc(box, x0, y0, more)} } ${b}%, ${c}% { ${sc(box, x1, y1, more)} } ${d}%, 100% { ${sc(box, x0, y0, more)} } }`;
|
|
63
|
+
|
|
43
64
|
export const SCENE_CSS = `
|
|
44
65
|
/* ── The stage in a run card ──────────────────────────────────────── */
|
|
45
66
|
.asst-run-stage-wrap {
|
|
@@ -110,8 +131,15 @@ export const SCENE_CSS = `
|
|
|
110
131
|
.asst-run.is-revealed { background: var(--asst-fill-1, rgba(255,255,255,.04)); box-shadow: 0 0 0 1px var(--asst-line-2, rgba(255,255,255,.12)); }
|
|
111
132
|
|
|
112
133
|
/* ── Scenes ─────────────────────────────────────────────────────────── */
|
|
113
|
-
/* --sc-gold is set by a holder that neutralizes it (waiting, settled); unset, it is the brand gold.
|
|
114
|
-
|
|
134
|
+
/* --sc-gold is set by a holder that neutralizes it (waiting, settled); unset, it is the brand gold.
|
|
135
|
+
The box takes its holder's size (120×40 and 40×20 by default); its still SVG and every layer cover
|
|
136
|
+
it, in the same viewBox; strokes are 1.5 units unless the holder says otherwise (they inherit). */
|
|
137
|
+
.asst-scene { position: relative; display: block; overflow: visible; stroke-width: 1.5; }
|
|
138
|
+
:where(.asst-scene) { width: 120px; height: 40px; }
|
|
139
|
+
:where(.asst-scene.is-mini) { width: 40px; height: 20px; }
|
|
140
|
+
.asst-scene svg { position: absolute; inset: 0; display: block; width: 100%; height: 100%; overflow: visible; }
|
|
141
|
+
.asst-scene .sc-layer { position: absolute; inset: 0; display: block; transform-origin: 0 0; pointer-events: none; }
|
|
142
|
+
.asst-scene .sc-clip { position: absolute; display: block; overflow: hidden; pointer-events: none; }
|
|
115
143
|
.asst-scene .sc-dim { stroke-opacity: .5; }
|
|
116
144
|
.asst-scene .sc-mid { stroke-opacity: .72; }
|
|
117
145
|
.asst-scene .sc-gold { stroke: var(--sc-gold, var(--asst-brand, rgba(255,215,0,.85))); }
|
|
@@ -120,9 +148,13 @@ export const SCENE_CSS = `
|
|
|
120
148
|
.asst-scene .sc-fill.sc-mid { fill-opacity: .55; }
|
|
121
149
|
.asst-scene .sc-fill.sc-gold { fill: var(--sc-gold, var(--asst-brand, rgba(255,215,0,.85))); }
|
|
122
150
|
.asst-scene .sc-shade { fill: var(--sc-shade, #131316); }
|
|
123
|
-
.asst-scene .sc-ring {
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
.asst-scene .sc-ring { opacity: 0; }
|
|
152
|
+
/* A layer that only turns or travels gets its compositor layer up front, and only while its scene is
|
|
153
|
+
live and on screen and motion is allowed (no standing hints). Layers that scale are left to the
|
|
154
|
+
browser, which rasterises them at the largest scale they reach (a hint would fix it at rest). */
|
|
155
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
156
|
+
.asst-scene[data-live]:not([data-offscreen] *) :is(.sc-gear-big, .sc-gear-small, .sc-hand, .sc-wrench, .sc-code-scroll, .sc-code-caret, .sc-lens, .sc-lens-mini, .sc-point-browser, .sc-point-computer, .sc-point-mini, .sc-run-dot, .sc-pen, .sc-film, .sc-film-mini, .sc-slide) { will-change: transform; }
|
|
157
|
+
}
|
|
126
158
|
.asst-scene[data-live] .sc-code-scroll { animation: asst-sc-code 6.6s cubic-bezier(.45, 0, .55, 1) infinite; }
|
|
127
159
|
.asst-scene[data-live] .sc-code-caret { animation: asst-sc-code-caret 6.6s cubic-bezier(.45, 0, .55, 1) infinite; }
|
|
128
160
|
.asst-scene[data-live] .sc-caret { animation: asst-rk-caret 1.06s steps(1) infinite; }
|
|
@@ -229,23 +261,23 @@ export const SCENE_CSS = `
|
|
|
229
261
|
${codeFrames()}
|
|
230
262
|
@keyframes asst-sc-spin { to { transform: rotate(360deg); } }
|
|
231
263
|
@keyframes asst-sc-tick { 0%, 8% { opacity: 0; transform: scale(.4); } 16%, 80% { opacity: 1; transform: scale(1); } 92%, 100% { opacity: 0; transform: scale(1); } }
|
|
232
|
-
@keyframes asst-sc-lens { 0%, 100% {
|
|
233
|
-
@keyframes asst-sc-lens-mini { 0%, 100% {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
@keyframes asst-sc-lens { 0%, 100% { ${sc(FULL, 0, 0)} } 25% { ${sc(FULL, 14, 4)} } 50% { ${sc(FULL, 48, 0)} } 75% { ${sc(FULL, 34, 5)} } }
|
|
265
|
+
@keyframes asst-sc-lens-mini { 0%, 100% { ${sc(MINI, 0, 0)} } 50% { ${sc(MINI, 12, 2)} } }
|
|
266
|
+
${pointFrames('asst-sc-point-browser', FULL, [92, 29], [26, 16])}
|
|
267
|
+
${pointFrames('asst-sc-point-computer', FULL, [90, 22.5], [32, 9])}
|
|
268
|
+
${pointFrames('asst-sc-point-mini', MINI, [29, 13], [12, 8], 'scale(.6)', [24, 48, 74, 98])}
|
|
237
269
|
@keyframes asst-sc-ripple { 0% { opacity: .9; transform: scale(.3); } 16% { opacity: 0; transform: scale(1.5); } 100% { opacity: 0; transform: scale(1.5); } }
|
|
238
|
-
@keyframes asst-sc-run { 0% { opacity: 0;
|
|
270
|
+
@keyframes asst-sc-run { 0% { opacity: 0; ${sc(FULL, 0, 0)} } 8%, 12% { opacity: 1; ${sc(FULL, 0, 0)} } 45%, 55% { ${sc(FULL, 23, 0)} } 88% { opacity: 1; ${sc(FULL, 46, 0)} } 100% { opacity: 0; ${sc(FULL, 46, 0)} } }
|
|
239
271
|
@keyframes asst-sc-shape { 0%, 6% { opacity: 0; transform: scale(.4); } 16%, 84% { opacity: 1; transform: scale(1); } 94%, 100% { opacity: 0; transform: scale(.9); } }
|
|
240
|
-
@keyframes asst-sc-pen { 0%, 8% {
|
|
272
|
+
@keyframes asst-sc-pen { 0%, 8% { ${sc(FULL, 47, 13)} } 18%, 24% { ${sc(FULL, 73, 13)} } 32%, 84% { ${sc(FULL, 100, 20)} } 100% { ${sc(FULL, 47, 13)} } }
|
|
241
273
|
@keyframes asst-sc-develop { 0% { opacity: .08; } 30%, 82% { opacity: 1; } 100% { opacity: .08; } }
|
|
242
274
|
@keyframes asst-sc-twinkle { 0%, 100% { opacity: .35; transform: scale(.7) rotate(0deg); } 50% { opacity: 1; transform: scale(1.1) rotate(45deg); } }
|
|
243
275
|
@keyframes asst-sc-zap { 0%, 100% { opacity: .45; transform: scale(.92); } 40% { opacity: 1; transform: scale(1.06); } }
|
|
244
|
-
@keyframes asst-sc-film { to {
|
|
245
|
-
@keyframes asst-sc-film-mini { to {
|
|
276
|
+
@keyframes asst-sc-film { to { ${sc(win(FILM_WINDOW), -24, 0)} } }
|
|
277
|
+
@keyframes asst-sc-film-mini { to { ${sc(win(FILM_MINI_WINDOW), -12, 0)} } }
|
|
246
278
|
@keyframes asst-sc-wrench { 0%, 100% { transform: rotate(-14deg); } 50% { transform: rotate(16deg); } }
|
|
247
279
|
@keyframes asst-sc-spark { 0%, 38% { opacity: 0; } 50% { opacity: 1; } 72%, 100% { opacity: 0; } }
|
|
248
|
-
@keyframes asst-sc-slide { 0%, 100% {
|
|
280
|
+
@keyframes asst-sc-slide { 0%, 100% { ${sc(FULL, 0, 0)} } 50% { ${sc(FULL, 68, 0)} } }
|
|
249
281
|
@keyframes asst-sc-dot { 0%, 100% { opacity: .3; } 40% { opacity: 1; } }
|
|
250
282
|
body.ui-interacting .asst-scene *,
|
|
251
283
|
body.ui-interacting .asst-run-stage-sweep::after { animation-play-state: paused !important; }
|