@songsid/agend 2.3.0-beta.6 → 2.3.0-beta.7
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/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +68 -8
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js +4 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +3 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +31 -11
- package/dist/authority/web-authority.js +39 -12
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +55 -3
- package/dist/cache-service.js +152 -16
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/mcp-tools.js +12 -6
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +2 -2
- package/dist/classic-channel-manager.js +2 -6
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +10 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +2 -0
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -0
- package/dist/delivery-outbox.js +7 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +109 -2
- package/dist/fleet-manager.js +280 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +81 -25
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +55 -8
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +14 -0
- package/dist/human-delivery-receipts.js +42 -6
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +8 -1
- package/dist/instructions.js +9 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +52 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +6 -0
- package/dist/outbound-handlers.js +45 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +57 -1
- package/dist/outbound-schemas.js +33 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +50 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +27 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +8 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/tool-permissions.d.ts +0 -7
- package/dist/tool-permissions.js +73 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -1
- package/dist/topic-commands.js +2 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/ui/app-effects.js +43 -25
- package/dist/ui/chat-ambient.js +306 -0
- package/dist/ui/chat-effect-posters.js +6 -7
- package/dist/ui/chat-effects.js +38 -15
- package/dist/ui/chat-strings.js +17 -4
- package/dist/ui/panel-chat.js +15 -7
- package/dist/ui/panel-settings.js +4 -1
- package/dist/ui/settings-appearance.js +8 -0
- package/dist/ui/settings-mods.js +19 -6
- package/dist/ui/settings-owner-effects.js +3 -3
- package/dist/ui/settings-strings.js +28 -0
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +2 -1
- package/dist/ui/shared/app-themes.js +24 -6
- package/dist/ui/shared/app.css +28 -0
- package/dist/ui/shared/effect-app-runtime.js +67 -18
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +17 -3
- package/dist/ui/shared/effect-policy.js +18 -4
- package/dist/ui/shared/effect-registry.js +11 -18
- package/dist/ui/shared/effect-stock-trust.js +2 -2
- package/dist/ui/shared/effect-trial-stage.js +18 -7
- package/dist/ui/shared/fx-motion.js +58 -0
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/usage-report.d.ts +147 -0
- package/dist/usage-report.js +195 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +154 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +32 -0
- package/dist/web-api.js +143 -18
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +18 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -2
- package/dist/web-appearance-view.js +32 -5
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +15 -1
- package/dist/web-appearance.js +77 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +2 -2
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +25 -4
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-guest-access.js +3 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-agent.js +5 -1
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +9 -1
- package/dist/web-mod-bundle.js +62 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -1
- package/dist/web-mod-store.js +13 -6
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +16 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +24 -2
- package/dist/web-settings-patch.js +47 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +8 -0
- package/dist/web-settings-tools.js +14 -1
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-theme-store.d.ts +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/app-effects.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/** #1629: one session-only app effect owner. Importing this module starts nothing. */
|
|
2
2
|
import { appStore } from "/assets/app-store.js";
|
|
3
3
|
import { navStore } from "/assets/app-nav.js";
|
|
4
|
-
import { onPrefsChanged
|
|
5
|
-
import { resolvedMode, themesStore } from "/assets/app-themes.js";
|
|
4
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
5
|
+
import { resolvedMode, themesStore, themesShown } from "/assets/app-themes.js";
|
|
6
6
|
import { ownerCssLifecycle, safeMode } from "/assets/owner-css.js";
|
|
7
7
|
import { constrainedDevice } from "/assets/effect-policy.js";
|
|
8
8
|
import { createAppEffectRuntime } from "/assets/effect-app-runtime.js";
|
|
9
|
-
import {
|
|
9
|
+
import { effectBootstrapReady } from "/assets/effect-frame.js";
|
|
10
10
|
import { createEffectCursor } from "/assets/effect-cursor.js";
|
|
11
11
|
import { createOwnerEffectRegions } from "/assets/effect-owner-regions.js";
|
|
12
|
-
import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
|
|
12
|
+
import { chatEffectIdentity, effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
|
|
13
13
|
import { createEffectTrialController } from "./effect-trial-controller.js";
|
|
14
14
|
import { effectTrialMessage } from "./effect-trial-offer.js";
|
|
15
15
|
import { claimPanel, panelVersion, panelStore, openTrialPanel, closePanel } from "./preview-panel.js";
|
|
@@ -20,8 +20,9 @@ export function getAppEffects(host = window) { const value = owners.get(host); r
|
|
|
20
20
|
export function startAppEffects({ store, window: host = window, factories = {} } = {}) {
|
|
21
21
|
const existing = owners.get(host);
|
|
22
22
|
if (existing) { if (!existing.dead) existing.bindStore(store); return getAppEffects(host); }
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const playbackIdentity = effectPlaybackIdentity(host), ownerIdentity = chatEffectIdentity(host);
|
|
24
|
+
if (!playbackIdentity || typeof host.AgendAuth?.onSessionEnd !== "function" || typeof host.AgendAuth?.sessionEnded !== "function") return null;
|
|
25
|
+
const doc = host.document, releases = [], ended = new Set(), updates = new Set(), trialUpdates = new Set(); let replyOff = null, replyStore = null, regions = null, cursor = null, cursorStamp = null, trialControl = null, trialIntent = null, trialHint = ownerIdentity ? null : false, trialSerial = 0, trialStamp = null, chatBinding = null;
|
|
25
26
|
const safe = fn => { try { return fn(); } catch { return null; } };
|
|
26
27
|
const hold = off => { if (entry.dead) safe(() => off?.()); else releases.push(off); };
|
|
27
28
|
const entry = { dead: false, api: null,
|
|
@@ -76,14 +77,33 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
76
77
|
},
|
|
77
78
|
};
|
|
78
79
|
owners.set(host, entry); // Reentrant setup never constructs another owner.
|
|
79
|
-
let media, coarse
|
|
80
|
+
let media, coarse;
|
|
81
|
+
function ownerCssForPlayback() {
|
|
82
|
+
if (doc.body.dataset.tier !== "guest") return ownerCssLifecycle.get().state;
|
|
83
|
+
// Guests never install owner CSS. Prove its absence rather than borrowing the uninstalled
|
|
84
|
+
// lifecycle's initial unknown, and refuse unexpected styles instead of trusting a missing id.
|
|
85
|
+
try {
|
|
86
|
+
if (doc.body.dataset.ownerCss || doc.querySelectorAll("#owner-css").length) return "unknown";
|
|
87
|
+
// A CSP-refused or disabled STYLE has no paint. Recheck on every proof so enabling
|
|
88
|
+
// it never borrows an earlier absence verdict (WebKit capture inserts a refused no-op).
|
|
89
|
+
for (const style of doc.querySelectorAll("style")) if (style.sheet && style.sheet.disabled !== true) return "unknown";
|
|
90
|
+
const allowed = new Set(["/assets/tokens.css", "/assets/app.css"]);
|
|
91
|
+
for (const link of doc.querySelectorAll('link[rel~="stylesheet"]')) {
|
|
92
|
+
const url = new URL(link.getAttribute("href"), host.location.origin);
|
|
93
|
+
if (url.origin !== host.location.origin || !allowed.has(url.pathname) || url.search || url.hash) return "unknown";
|
|
94
|
+
}
|
|
95
|
+
return "off";
|
|
96
|
+
} catch { return "unknown"; }
|
|
97
|
+
}
|
|
98
|
+
function guestChatVersion(value) {
|
|
99
|
+
if (doc.body.dataset.tier !== "guest") return null;
|
|
100
|
+
const name = navStore.get().route?.instance, v = value.instances?.find(i => i.name === name)?.effect_chat_v;
|
|
101
|
+
return typeof v === "string" && /^[a-f0-9]{32}$/.test(v) ? v : null;
|
|
102
|
+
}
|
|
80
103
|
function context() {
|
|
81
|
-
let ready = false; try { ready = preview?.availability().ok === true; } catch { /* unavailable */ }
|
|
82
104
|
return { safe: safeMode(), publicLink: doc.body.dataset.publicLink === "1", reducedMotion: media?.matches !== false, saveData: host.navigator?.connection?.saveData === true,
|
|
83
|
-
hidden: doc.visibilityState !== "visible", enabled: typeof host.OffscreenCanvas === "function" && typeof host.Worker === "function" && typeof host.MutationObserver === "function" && typeof host.ResizeObserver === "function",
|
|
84
|
-
show:
|
|
85
|
-
trialReady: effectTransportReady(host, doc.body.dataset.previewOrigin, doc.body.dataset.previewBoot),
|
|
86
|
-
optIn: prefInForce("preview.optIn")?.value === true && preview?.optedIn() === true && preview?.never() !== true,
|
|
105
|
+
hidden: doc.visibilityState !== "visible", enabled: entry.api?.playbackAvailable !== false && typeof host.OffscreenCanvas === "function" && typeof host.Worker === "function" && typeof host.MutationObserver === "function" && typeof host.ResizeObserver === "function",
|
|
106
|
+
show: themesShown(), transportReady: effectBootstrapReady(host, host.location?.origin, doc.body.dataset.effectBoot),
|
|
87
107
|
constrained: constrainedDevice({ memory: host.navigator?.deviceMemory, cores: host.navigator?.hardwareConcurrency, coarse: coarse?.matches === true, width: host.innerWidth }),
|
|
88
108
|
modal: !!doc.querySelector("dialog[open],.menu-list,.pop,[role=alertdialog],.oc-keep,.shell.sb-open"), openInstance: null };
|
|
89
109
|
}
|
|
@@ -100,7 +120,7 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
100
120
|
if (c.reducedMotion) return "reducedMotion";
|
|
101
121
|
if (c.saveData) return "saveData";
|
|
102
122
|
if (!c.enabled) return "unsupported";
|
|
103
|
-
if (!c.
|
|
123
|
+
if (!c.transportReady) return "transport";
|
|
104
124
|
if (c.publicLink) return "localAdmin";
|
|
105
125
|
if (c.hidden || doc.hasFocus?.() !== true) return "inactive";
|
|
106
126
|
if (c.modal) return "dialog";
|
|
@@ -109,7 +129,7 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
109
129
|
function trialAllowed() {
|
|
110
130
|
const c = context();
|
|
111
131
|
return !entry.dead && doc.visibilityState === "visible" && doc.hasFocus?.() === true
|
|
112
|
-
&& c.enabled && c.show && c.
|
|
132
|
+
&& c.enabled && c.show && c.transportReady && !c.safe && !c.publicLink && !c.reducedMotion && !c.saveData && !c.hidden && !c.modal;
|
|
113
133
|
}
|
|
114
134
|
function mountTrial(handle) {
|
|
115
135
|
const intent = trialIntent; if (!intent || !trialCurrent(handle.instance) || !handle.current() || !Number.isSafeInteger(++trialSerial)) throw Error("trial_unavailable");
|
|
@@ -144,47 +164,45 @@ export function startAppEffects({ store, window: host = window, factories = {} }
|
|
|
144
164
|
function syncCursor() {
|
|
145
165
|
if (entry.dead || !cursor) return;
|
|
146
166
|
const c = context();
|
|
147
|
-
cursor.update(entry.api?.cursorSelection?.() ?? null, c.enabled && c.show && c.
|
|
167
|
+
cursor.update(entry.api?.cursorSelection?.() ?? null, c.enabled && c.show && c.transportReady && !c.safe && !c.publicLink && !c.reducedMotion && !c.saveData && !c.hidden && !c.modal && !cursorDragging());
|
|
148
168
|
}
|
|
149
169
|
try {
|
|
150
170
|
if (appStore.get().connection === "ended") { entry.dispose(); return null; }
|
|
151
|
-
preview = host.AgendPreview;
|
|
152
171
|
media = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
153
172
|
coarse = host.matchMedia?.("(pointer: coarse)") ?? { matches: false };
|
|
154
173
|
entry.api = (factories.runtime ?? createAppEffectRuntime)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
155
|
-
observe: () => ({ context: context(), ownerCss:
|
|
174
|
+
observe: () => ({ context: context(), ownerCss: ownerCssForPlayback(), mode: resolvedMode() }), onDispose: () => entry.dispose(),
|
|
156
175
|
resolveOwnerTarget: (slot, manifest, proof) => !entry.dead ? regions?.resolve?.(slot, manifest.box, proof) ?? null : null,
|
|
157
176
|
onChange: () => { syncCursor(); notify(); } });
|
|
158
177
|
if (entry.dead || !entry.api.ok) { entry.dispose(); return null; }
|
|
159
|
-
trialControl = (factories.trialController ?? createEffectTrialController)({ window: host, current: trialCurrent, allowed: trialAllowed, onStart: mountTrial, onChange: notify });
|
|
178
|
+
if (ownerIdentity) trialControl = (factories.trialController ?? createEffectTrialController)({ window: host, current: trialCurrent, allowed: trialAllowed, onStart: mountTrial, onChange: notify });
|
|
160
179
|
hold(() => trialControl?.dispose());
|
|
161
|
-
if (typeof host.AgendAuth.trialSession === "function") Promise.resolve().then(() => host.AgendAuth.trialSession()).then(value => {
|
|
180
|
+
if (ownerIdentity && typeof host.AgendAuth.trialSession === "function") Promise.resolve().then(() => host.AgendAuth.trialSession()).then(value => {
|
|
162
181
|
if (entry.dead) return; trialHint = value?.localAdmin === true && value.epoch === chatEffectIdentity(host)?.epoch; notify();
|
|
163
182
|
}).catch(() => { if (!entry.dead) { trialHint = false; notify(); } });
|
|
164
183
|
hold(panelStore.subscribe(() => { trialControl?.sync(); notifyTrial(); }));
|
|
165
|
-
const ownRegions = (factories.regions ?? createOwnerEffectRegions)({ window: host, current: () => !entry.dead && host.document === doc });
|
|
184
|
+
const ownRegions = ownerIdentity && (factories.regions ?? createOwnerEffectRegions)({ window: host, current: () => !entry.dead && host.document === doc });
|
|
166
185
|
regions = ownRegions?.ok ? ownRegions : null; hold(() => ownRegions?.dispose?.());
|
|
167
186
|
if (entry.dead) return null;
|
|
168
|
-
const ownCursor = safe(() => (factories.cursor ?? createEffectCursor)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
187
|
+
const ownCursor = ownerIdentity && safe(() => (factories.cursor ?? createEffectCursor)({ window: host, current: () => !entry.dead && host.document === doc,
|
|
169
188
|
readImage: (selection, signal) => entry.api?.cursorImageSettled?.(selection, signal) ?? Promise.resolve(null),
|
|
170
189
|
onChange(value) { const next = JSON.stringify(value); if (next !== cursorStamp) { cursorStamp = next; notify(); } },
|
|
171
190
|
}));
|
|
172
191
|
cursor = ownCursor?.ok ? ownCursor : null; hold(() => ownCursor?.dispose?.());
|
|
173
192
|
if (entry.dead) return null;
|
|
174
193
|
const sync = () => { if (!entry.dead) { trialControl?.sync(); entry.api.sync(); syncCursor(); notifyTrial(); } };
|
|
175
|
-
let revision = appStore.get().themesRev, prefsRevision = appStore.get().prefsRev, navSeq = navStore.get().seq;
|
|
194
|
+
let revision = appStore.get().themesRev, prefsRevision = appStore.get().prefsRev, guestRevision = guestChatVersion(appStore.get()), navSeq = navStore.get().seq;
|
|
176
195
|
hold(navStore.subscribe(value => { if (entry.dead || value.seq === navSeq) return; navSeq = value.seq; trialControl?.stop(); chatBinding = null; entry.api.detachChat(); syncCursor(); }));
|
|
177
196
|
hold(appStore.subscribe(value => {
|
|
178
197
|
if (entry.dead) return;
|
|
179
198
|
if (value.connection === "ended") { entry.dispose(); return; }
|
|
180
|
-
const chatChanged = value.themesRev !== revision, ownerChanged = value.prefsRev !== prefsRevision;
|
|
181
|
-
revision = value.themesRev; prefsRevision = value.prefsRev;
|
|
199
|
+
const guestNext = guestChatVersion(value), chatChanged = value.themesRev !== revision || guestNext !== guestRevision, ownerChanged = value.prefsRev !== prefsRevision;
|
|
200
|
+
revision = value.themesRev; prefsRevision = value.prefsRev; guestRevision = guestNext;
|
|
182
201
|
if (chatChanged) entry.api.changedChat();
|
|
183
202
|
if (ownerChanged) entry.api.changedOwner();
|
|
184
203
|
if (!chatChanged && !ownerChanged) sync();
|
|
185
204
|
}));
|
|
186
205
|
hold(ownerCssLifecycle.subscribe(sync)); hold(themesStore.subscribe(sync)); hold(onPrefsChanged(sync));
|
|
187
|
-
if (typeof preview?.onChange === "function") hold(preview.onChange(sync));
|
|
188
206
|
if (typeof host.AgendTheme?.onChange === "function") hold(host.AgendTheme.onChange(sync));
|
|
189
207
|
for (const [target, type] of [[media, "change"], [coarse, "change"], [doc, "visibilitychange"], [host, "resize"], [host.navigator?.connection, "change"]]) {
|
|
190
208
|
if (target?.addEventListener) { target.addEventListener(type, sync); hold(() => target.removeEventListener(type, sync)); }
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// #2117: an agent's ambient effect on its own chat — falling petals, rising lanterns, twinkles, stars on a click. The
|
|
2
|
+
// agent sends data only (web-ambient.ts checks it; this checks the bounds again); the page draws it with plain nodes
|
|
3
|
+
// animated by the Web Animations API: transform and opacity only, positions through the CSSOM (no style attribute,
|
|
4
|
+
// #1300), every node aria-hidden and pointer-events: none. The looping layer sits behind the thread (.amb-layer, under
|
|
5
|
+
// the scroller and the dock); a burst shows over the messages for a moment (.amb-burst) and never takes the click.
|
|
6
|
+
//
|
|
7
|
+
// Its own gates (not the Canvas Worker runtime's, #2115): the viewer's "Show agents' themes, effects and widgets", safe
|
|
8
|
+
// mode, Web Animations support, and reduced motion only when this device chose to follow the system's setting
|
|
9
|
+
// (fx-motion.js; the user's direction: effects play by default). A hidden tab pauses. Nothing else gates it: it runs
|
|
10
|
+
// no agent code and fetches nothing but its own glyph pictures (fleet decision: no gate without a threat it stops).
|
|
11
|
+
// Caps: 24 looping particles, bursts of at most 16, at most 3 bursts alive — 72 nodes and animations per chat.
|
|
12
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
13
|
+
import { t } from "/assets/app-i18n.js";
|
|
14
|
+
import { useStore } from "/assets/app-store.js";
|
|
15
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
16
|
+
import { safeMode } from "/assets/owner-css.js";
|
|
17
|
+
import { setThemesShown, themeImageUrl, themesShown, themesStore } from "/assets/app-themes.js";
|
|
18
|
+
import { markMotionNoteShown, motionBlocked, motionNoteDue, onMotionChange, setFollowsReducedMotion } from "/assets/fx-motion.js";
|
|
19
|
+
import "./chat-strings.js";
|
|
20
|
+
|
|
21
|
+
export const AMBIENT_CAPS = { loop: 24, burst: 16, bursts: 3 };
|
|
22
|
+
const LOOP_PRESETS = ["fall", "rise", "twinkle"];
|
|
23
|
+
const RULES = {
|
|
24
|
+
fall: { count: [1, 24], duration: [4, 30] }, rise: { count: [1, 24], duration: [4, 30] },
|
|
25
|
+
twinkle: { count: [1, 24], duration: [1, 10] }, "burst-on-click": { count: [4, 16], duration: [0.3, 2] },
|
|
26
|
+
};
|
|
27
|
+
const COLOR = /^#[0-9a-f]{6}$/;
|
|
28
|
+
const ASSET = /^[0-9a-f]{32}$/;
|
|
29
|
+
const own = (o, k) => !!o && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
30
|
+
const num = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
|
|
31
|
+
const pair = (v, lo, hi) => Array.isArray(v) && v.length === 2 && num(v[0], lo, hi) && num(v[1], lo, hi) && v[0] <= v[1];
|
|
32
|
+
|
|
33
|
+
/** A spec as the host keeps it, checked again; null when anything is off (it is then not drawn at all). */
|
|
34
|
+
export function sanitizeAmbient(spec) {
|
|
35
|
+
if (!spec || typeof spec !== "object" || !own(RULES, spec.preset)) return null;
|
|
36
|
+
const r = RULES[spec.preset];
|
|
37
|
+
if (!Array.isArray(spec.glyphs) || spec.glyphs.length < 1 || spec.glyphs.length > 4) return null;
|
|
38
|
+
const glyphs = [];
|
|
39
|
+
for (const g of spec.glyphs) {
|
|
40
|
+
if (typeof g === "string" && g.length >= 1 && g.length <= 32) glyphs.push(g);
|
|
41
|
+
else if (g && typeof g === "object" && typeof g.asset === "string" && ASSET.test(g.asset)) glyphs.push({ asset: g.asset });
|
|
42
|
+
else return null;
|
|
43
|
+
}
|
|
44
|
+
const colors = Array.isArray(spec.colors) ? spec.colors.filter((c) => typeof c === "string" && COLOR.test(c)).slice(0, 5) : [];
|
|
45
|
+
if (!Number.isInteger(spec.count) || !num(spec.count, r.count[0], r.count[1])) return null;
|
|
46
|
+
if (!pair(spec.duration, r.duration[0], r.duration[1]) || !pair(spec.size, 10, 40) || !pair(spec.spread, 20, 120)) return null;
|
|
47
|
+
if (!num(spec.sway, 0, 80) || !num(spec.rotate, 0, 720)) return null;
|
|
48
|
+
if (!Array.isArray(spec.opacity) || spec.opacity.length !== 3 || !spec.opacity.every((o) => num(o, 0, 1))) return null;
|
|
49
|
+
return { preset: spec.preset, glyphs, colors, count: spec.count, duration: spec.duration, size: spec.size, sway: spec.sway, spread: spec.spread, rotate: spec.rotate, opacity: spec.opacity };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `name`'s ambients from GET /ui/themes, checked: { loop, click } (either null). Null when it has none. */
|
|
53
|
+
export function ambientFor(name) {
|
|
54
|
+
const a = themesStore.get().ambient;
|
|
55
|
+
if (!a || typeof a !== "object" || !own(a, name)) return null;
|
|
56
|
+
const mine = a[name];
|
|
57
|
+
const loop = own(mine, "loop") ? sanitizeAmbient(mine.loop && mine.loop.spec) : null;
|
|
58
|
+
const click = own(mine, "click") ? sanitizeAmbient(mine.click && mine.click.spec) : null;
|
|
59
|
+
if (loop && !LOOP_PRESETS.includes(loop.preset)) return null;
|
|
60
|
+
if (click && click.preset !== "burst-on-click") return null;
|
|
61
|
+
return loop || click ? { loop, click } : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Why this page does not draw ambients now, or null. */
|
|
65
|
+
export function ambientBlock(win = globalThis) {
|
|
66
|
+
if (safeMode()) return "safe";
|
|
67
|
+
if (!themesShown()) return "hidden";
|
|
68
|
+
if (motionBlocked(win)) return "reducedMotion";
|
|
69
|
+
const El = win.Element;
|
|
70
|
+
if (!El || !El.prototype || typeof El.prototype.animate !== "function") return "unsupported";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The gate as state: moves with the switches, the themes and the device's reduced-motion answer. */
|
|
75
|
+
function useAmbientBlock() {
|
|
76
|
+
useStore(themesStore);
|
|
77
|
+
const [, bump] = useState(0);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const again = () => bump((n) => n + 1);
|
|
80
|
+
const offPrefs = onPrefsChanged((changed) => { if (changed.has("fx.show")) again(); });
|
|
81
|
+
const offMotion = onMotionChange(again);
|
|
82
|
+
return () => { if (typeof offPrefs === "function") offPrefs(); offMotion(); };
|
|
83
|
+
}, []);
|
|
84
|
+
return ambientBlock();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rand = (lo, hi) => lo + Math.random() * (hi - lo);
|
|
88
|
+
const pick = (list) => list[Math.floor(Math.random() * list.length)];
|
|
89
|
+
|
|
90
|
+
/** One particle node: a text glyph (its colour, else the accent) or a glyph picture. */
|
|
91
|
+
function particle(doc, name, spec, cls) {
|
|
92
|
+
const g = pick(spec.glyphs), size = Math.round(rand(spec.size[0], spec.size[1]));
|
|
93
|
+
let el;
|
|
94
|
+
if (typeof g === "string") {
|
|
95
|
+
el = doc.createElement("span");
|
|
96
|
+
el.textContent = g;
|
|
97
|
+
el.style.setProperty("font-size", `${size}px`);
|
|
98
|
+
if (spec.colors.length) el.style.setProperty("color", pick(spec.colors));
|
|
99
|
+
} else {
|
|
100
|
+
el = doc.createElement("img");
|
|
101
|
+
el.src = themeImageUrl(name, g.asset);
|
|
102
|
+
el.alt = "";
|
|
103
|
+
el.decoding = "async";
|
|
104
|
+
el.draggable = false;
|
|
105
|
+
el.style.setProperty("width", `${size}px`);
|
|
106
|
+
el.style.setProperty("height", `${size}px`);
|
|
107
|
+
}
|
|
108
|
+
el.className = cls;
|
|
109
|
+
el.setAttribute("aria-hidden", "true");
|
|
110
|
+
return { el, size };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Draw `ambient` ({ loop, click }) for `name` into `layer` (behind) and `front` (bursts), listening on `surface` for
|
|
115
|
+
* clicks. Returns { stop() }: every node and animation it made goes, every listener it added is removed.
|
|
116
|
+
*/
|
|
117
|
+
export function startAmbient({ name, ambient, layer, front, surface, doc = globalThis.document }) {
|
|
118
|
+
const anims = new Set();
|
|
119
|
+
const loopNodes = [];
|
|
120
|
+
let bursts = 0, stopped = false;
|
|
121
|
+
const track = (a) => { anims.add(a); if (doc.visibilityState === "hidden") a.pause(); return a; };
|
|
122
|
+
|
|
123
|
+
const drawLoop = () => {
|
|
124
|
+
for (const n of loopNodes.splice(0)) n.remove();
|
|
125
|
+
for (const a of [...anims]) if (a._amb === "loop") { a.cancel(); anims.delete(a); }
|
|
126
|
+
const spec = ambient.loop;
|
|
127
|
+
if (!spec) return;
|
|
128
|
+
const h = layer.clientHeight || 600, w = layer.clientWidth || 360;
|
|
129
|
+
const [o0, o1, o2] = spec.opacity;
|
|
130
|
+
for (let i = 0; i < Math.min(spec.count, AMBIENT_CAPS.loop); i++) {
|
|
131
|
+
const { el, size } = particle(doc, name, spec, "amb-p");
|
|
132
|
+
const dur = rand(spec.duration[0], spec.duration[1]) * 1000;
|
|
133
|
+
const turn = spec.rotate * (Math.random() < 0.5 ? -1 : 1);
|
|
134
|
+
let frames;
|
|
135
|
+
if (spec.preset === "twinkle") {
|
|
136
|
+
el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
|
|
137
|
+
el.style.setProperty("top", `${rand(0, Math.max(0, h - size))}px`);
|
|
138
|
+
frames = [
|
|
139
|
+
{ transform: "scale(0.6) rotate(0deg)", opacity: o0 },
|
|
140
|
+
{ transform: `scale(1) rotate(${turn / 2}deg)`, opacity: o1, offset: 0.5 },
|
|
141
|
+
{ transform: `scale(0.6) rotate(${turn}deg)`, opacity: o2 },
|
|
142
|
+
];
|
|
143
|
+
} else {
|
|
144
|
+
el.style.setProperty("left", `${rand(0, Math.max(0, w - size))}px`);
|
|
145
|
+
el.style.setProperty("top", "0px");
|
|
146
|
+
const dx = rand(-spec.sway, spec.sway), down = spec.preset === "fall";
|
|
147
|
+
const from = down ? -size - 4 : h + 4, to = down ? h + 4 : -size - 4;
|
|
148
|
+
frames = [
|
|
149
|
+
{ transform: `translate(0px, ${from}px) rotate(0deg)`, opacity: o0 },
|
|
150
|
+
{ transform: `translate(${dx * 0.6}px, ${from + (to - from) * 0.15}px) rotate(${turn * 0.15}deg)`, opacity: o1, offset: 0.15 },
|
|
151
|
+
{ transform: `translate(${-dx * 0.4}px, ${from + (to - from) * 0.6}px) rotate(${turn * 0.6}deg)`, opacity: (o1 + o2) / 2, offset: 0.6 },
|
|
152
|
+
{ transform: `translate(${dx}px, ${to}px) rotate(${turn}deg)`, opacity: o2 },
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
layer.appendChild(el);
|
|
156
|
+
loopNodes.push(el);
|
|
157
|
+
// A negative delay starts each one part-way through, so the layer is already full when it appears.
|
|
158
|
+
const a = el.animate(frames, { duration: dur, delay: -Math.random() * dur, iterations: Infinity, easing: "linear" });
|
|
159
|
+
a._amb = "loop";
|
|
160
|
+
track(a);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const burst = (x, y) => {
|
|
165
|
+
const spec = ambient.click;
|
|
166
|
+
if (!spec || bursts >= AMBIENT_CAPS.bursts || stopped) return;
|
|
167
|
+
bursts++;
|
|
168
|
+
const [, o1, o2] = spec.opacity;
|
|
169
|
+
let left = Math.min(spec.count, AMBIENT_CAPS.burst);
|
|
170
|
+
const done = (el, a) => { el.remove(); anims.delete(a); if (--left === 0) bursts--; };
|
|
171
|
+
for (let i = 0, n = left; i < n; i++) {
|
|
172
|
+
const { el, size } = particle(doc, name, spec, "amb-p");
|
|
173
|
+
el.style.setProperty("left", `${x - size / 2}px`);
|
|
174
|
+
el.style.setProperty("top", `${y - size / 2}px`);
|
|
175
|
+
const angle = (i / n) * Math.PI * 2 + rand(-0.3, 0.3), dist = rand(spec.spread[0], spec.spread[1]);
|
|
176
|
+
const turn = rand(-spec.rotate, spec.rotate) / 2;
|
|
177
|
+
front.appendChild(el);
|
|
178
|
+
const a = el.animate([
|
|
179
|
+
{ transform: "translate(0px, 0px) scale(0.4) rotate(0deg)", opacity: o1 },
|
|
180
|
+
{ transform: `translate(${Math.cos(angle) * dist}px, ${Math.sin(angle) * dist}px) scale(1) rotate(${turn}deg)`, opacity: Math.min(o2, o1) },
|
|
181
|
+
], { duration: rand(spec.duration[0], spec.duration[1]) * 1000, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" });
|
|
182
|
+
a._amb = "burst";
|
|
183
|
+
track(a);
|
|
184
|
+
let gone = false;
|
|
185
|
+
const finish = () => { if (!gone) { gone = true; done(el, a); } };
|
|
186
|
+
a.onfinish = finish; a.oncancel = finish;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Passive and never cancelling: the click still lands on what is under it. Only in the thread (not the composer),
|
|
191
|
+
// and not on a menu or dialog open over it: stars are for the conversation.
|
|
192
|
+
const onDown = (ev) => {
|
|
193
|
+
if (!ambient.click || ev.button > 0) return;
|
|
194
|
+
const target = ev.target;
|
|
195
|
+
if (!target || typeof target.closest !== "function" || !target.closest(".scroller") || target.closest("[role=menu], [role=dialog]")) return;
|
|
196
|
+
const box = front.getBoundingClientRect();
|
|
197
|
+
burst(ev.clientX - box.left, ev.clientY - box.top);
|
|
198
|
+
};
|
|
199
|
+
const onVisible = () => { for (const a of anims) { if (doc.visibilityState === "hidden") a.pause(); else a.play(); } };
|
|
200
|
+
// Size moves (a rotated phone, the preview panel opening): the loop is drawn again for the new box, a moment later.
|
|
201
|
+
let resizeTimer = null, lastW = layer.clientWidth, lastH = layer.clientHeight;
|
|
202
|
+
const ro = typeof globalThis.ResizeObserver === "function" ? new globalThis.ResizeObserver(() => {
|
|
203
|
+
if (layer.clientWidth === lastW && layer.clientHeight === lastH) return;
|
|
204
|
+
lastW = layer.clientWidth; lastH = layer.clientHeight;
|
|
205
|
+
clearTimeout(resizeTimer);
|
|
206
|
+
resizeTimer = setTimeout(() => { if (!stopped) drawLoop(); }, 200);
|
|
207
|
+
}) : null;
|
|
208
|
+
|
|
209
|
+
drawLoop();
|
|
210
|
+
if (ambient.click) surface.addEventListener("pointerdown", onDown, { passive: true });
|
|
211
|
+
doc.addEventListener("visibilitychange", onVisible);
|
|
212
|
+
if (ro && ambient.loop) ro.observe(layer);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
stop() {
|
|
216
|
+
stopped = true;
|
|
217
|
+
clearTimeout(resizeTimer);
|
|
218
|
+
if (ro) ro.disconnect();
|
|
219
|
+
surface.removeEventListener("pointerdown", onDown, { passive: true });
|
|
220
|
+
doc.removeEventListener("visibilitychange", onVisible);
|
|
221
|
+
for (const a of anims) a.cancel();
|
|
222
|
+
anims.clear();
|
|
223
|
+
for (const n of loopNodes.splice(0)) n.remove();
|
|
224
|
+
while (front.firstChild) front.firstChild.remove();
|
|
225
|
+
},
|
|
226
|
+
/** For tests and the smoke: what is alive now. */
|
|
227
|
+
stats() { return { nodes: layer.children.length + front.children.length, animations: anims.size, bursts }; },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The chat's ambient: renders its two layers inside .chat-main (`surface`) and runs them. `part` is "layer" (behind,
|
|
233
|
+
* right after the theme picture) or "front" (after the dock); the "layer" instance owns the run and finds its sibling.
|
|
234
|
+
*/
|
|
235
|
+
export function ChatAmbient({ name, surface, part }) {
|
|
236
|
+
const block = useAmbientBlock();
|
|
237
|
+
const ambient = block ? null : ambientFor(name);
|
|
238
|
+
const key = ambient ? JSON.stringify(ambient) : "";
|
|
239
|
+
const ref = useRef(null);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (part !== "layer" || !ambient) return undefined;
|
|
242
|
+
const layer = ref.current, host = surface.current;
|
|
243
|
+
const front = host && host.querySelector(".amb-burst");
|
|
244
|
+
if (!layer || !host || !front) return undefined;
|
|
245
|
+
host.classList.add("has-amb");
|
|
246
|
+
const run = startAmbient({ name, ambient, layer, front, surface: host });
|
|
247
|
+
if (typeof globalThis !== "undefined") globalThis.__agendAmbient = run; // the smoke reads stats(); nothing else
|
|
248
|
+
return () => { run.stop(); host.classList.remove("has-amb"); if (globalThis.__agendAmbient === run) delete globalThis.__agendAmbient; };
|
|
249
|
+
}, [name, key, part]);
|
|
250
|
+
if (!ambient) return null;
|
|
251
|
+
return part === "layer" ? html`<div ref=${ref} class="amb-layer" aria-hidden="true"></div>` : html`<div class="amb-burst" aria-hidden="true"></div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The one-time note (the user's direction): the system asks for reduced motion and this chat's ambient plays anyway —
|
|
256
|
+
* say so once on this device, and turn it off right here (this device's switch: the same on the public link, where
|
|
257
|
+
* Settings is not). Shown only where an ambient is actually drawn.
|
|
258
|
+
*/
|
|
259
|
+
export function AmbientMotionNote({ name }) {
|
|
260
|
+
const block = useAmbientBlock();
|
|
261
|
+
const shown = !block && !!ambientFor(name);
|
|
262
|
+
const [open, setOpen] = useState(false);
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (!shown || open || !motionNoteDue()) return;
|
|
265
|
+
markMotionNoteShown();
|
|
266
|
+
setOpen(true);
|
|
267
|
+
}, [shown]);
|
|
268
|
+
if (!open || !shown) return null;
|
|
269
|
+
const off = () => { setOpen(false); setFollowsReducedMotion(true); };
|
|
270
|
+
return html`<div class="amb-note" role="status">
|
|
271
|
+
<span>${t("chat.ambientMotionNote")}</span>
|
|
272
|
+
<button type="button" class="amb-note-off" onClick=${off}>${t("chat.ambientMotionNoteLink")}</button>
|
|
273
|
+
<button type="button" class="amb-note-x" aria-label=${t("chat.ambientMotionNoteClose")} onClick=${() => setOpen(false)}>×</button>
|
|
274
|
+
</div>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The fix for a block, done right here: both switches are this viewer's own (the show switch is a local choice on the
|
|
279
|
+
* public link; the reduced-motion one is per device), so it works on the public link too; safe mode is the URL. Both
|
|
280
|
+
* stay in Settings → Web appearance as well.
|
|
281
|
+
*/
|
|
282
|
+
const FIX = {
|
|
283
|
+
hidden: () => setThemesShown(true),
|
|
284
|
+
reducedMotion: () => setFollowsReducedMotion(false),
|
|
285
|
+
safe: () => { const u = new URL(globalThis.location.href); u.searchParams.delete("safe"); globalThis.location.assign(u.toString()); },
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The chat ⋯ entries for `name`'s ambient: why it is not showing here (a visible reason; choosing it fixes it when the
|
|
290
|
+
* page can), and — for an admin, never over the public link — Clear for each kind.
|
|
291
|
+
*/
|
|
292
|
+
export function ambientMenuItems(name, { publicLink, clear }) {
|
|
293
|
+
const a = themesStore.get().ambient;
|
|
294
|
+
const mine = a && own(a, name) ? a[name] : null;
|
|
295
|
+
if (!mine || typeof mine !== "object" || !(own(mine, "loop") || own(mine, "click"))) return [];
|
|
296
|
+
const items = [];
|
|
297
|
+
const block = ambientBlock();
|
|
298
|
+
if (block) {
|
|
299
|
+
items.push({
|
|
300
|
+
key: "ambient-blocked", label: t(`chat.ambientBlocked_${block}`), icon: "info",
|
|
301
|
+
onSelect: () => { if (own(FIX, block)) FIX[block](); },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (!publicLink) for (const k of ["loop", "click"]) if (own(mine, k)) items.push({ key: `ambient-clear-${k}`, label: t(k === "loop" ? "chat.ambientClearLoop" : "chat.ambientClearClick"), icon: "trash", onSelect: () => clear(name, k) });
|
|
305
|
+
return items;
|
|
306
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** #1629: static Chat fallback. It requires no preview origin, device consent, Worker or personal identity. */
|
|
2
2
|
import { useLayoutEffect } from "/assets/app-html.js";
|
|
3
3
|
import { appStore } from "/assets/app-store.js";
|
|
4
|
-
import { onPrefsChanged
|
|
4
|
+
import { onPrefsChanged } from "/assets/app-prefs.js";
|
|
5
5
|
import { themesShown, themesStore } from "/assets/app-themes.js";
|
|
6
6
|
import { ownerCssLifecycle } from "/assets/owner-css.js";
|
|
7
|
-
import {
|
|
7
|
+
import { effectPlaybackIdentity } from "/assets/effect-chat-runtime.js";
|
|
8
|
+
import { effectBootstrapReady } from "/assets/effect-frame.js";
|
|
8
9
|
import { createChatPosters } from "/assets/effect-posters.js";
|
|
9
10
|
|
|
10
11
|
export function ChatEffectPosters({ name, lease, element }) {
|
|
@@ -15,12 +16,11 @@ export function ChatEffectPosters({ name, lease, element }) {
|
|
|
15
16
|
const cleanup = () => { if (!live) return; live = false; try { api?.dispose?.(); } catch { /* optional */ } for (const off of releases) { try { off?.(); } catch { /* release all */ } } };
|
|
16
17
|
lease.hold(cleanup);
|
|
17
18
|
try {
|
|
18
|
-
const
|
|
19
|
+
const motion = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
19
20
|
const observe = () => {
|
|
20
|
-
let previewReady = false; try { previewReady = preview?.availability().ok === true; } catch { /* fallback */ }
|
|
21
21
|
// This is a surface fallback, not a replacement for an explicit Pause, queued effect or failed live read.
|
|
22
|
-
const playback =
|
|
23
|
-
|
|
22
|
+
const playback = effectPlaybackIdentity(host) && typeof host.Worker === "function" && typeof host.OffscreenCanvas === "function" &&
|
|
23
|
+
effectBootstrapReady(host, host.location?.origin, host.document.body.dataset.effectBoot) && !motion.matches && host.navigator?.connection?.saveData !== true;
|
|
24
24
|
return { shown: themesShown(), fallback: !playback, ownerCss: ownerCssLifecycle.get().state };
|
|
25
25
|
};
|
|
26
26
|
api = createChatPosters({ window: host, name, element: element.current, current, observe });
|
|
@@ -33,7 +33,6 @@ export function ChatEffectPosters({ name, lease, element }) {
|
|
|
33
33
|
if (s.themesRev !== revision) { revision = s.themesRev; api.changed(); } else sync();
|
|
34
34
|
}));
|
|
35
35
|
releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync));
|
|
36
|
-
if (typeof preview?.onChange === "function") releases.push(preview.onChange(sync));
|
|
37
36
|
for (const target of [motion, host.navigator?.connection]) if (target?.addEventListener) { target.addEventListener("change", sync); releases.push(() => target.removeEventListener("change", sync)); }
|
|
38
37
|
sync(); return cleanup;
|
|
39
38
|
} catch { cleanup(); return cleanup; }
|