import type { Applet, AppletEvent } from "@web-applets/sdk"; import { applets } from "@web-applets/sdk"; import { type ElementConfigurator, h } from "spellcaster/hyperscript.js"; import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js"; import { xxh32 } from "xxh32"; //////////////////////////////////////////// // 🪟 Applet initialiser //////////////////////////////////////////// export async function applet( src: string, opts: { addSlashSuffix?: boolean; context?: Window; container?: HTMLElement | Element; id?: string; setHeight?: boolean; } = {}, ): Promise> { src = `${src}${ src.endsWith("/") ? "" : opts.addSlashSuffix === undefined || opts.addSlashSuffix === true ? "/" : "" }`; const existingFrame: HTMLIFrameElement | null = (opts.context || window).document.querySelector( `[src="${src}"]`, ); let frame; if (existingFrame) { frame = existingFrame; } else { frame = document.createElement("iframe"); frame.src = src; if (opts.id) frame.id = opts.id; if (opts.container) { opts.container.appendChild(frame); } else { (opts.context || window).document.body.appendChild(frame); } } if (frame.contentWindow === null) { throw new Error("iframe does not have a contentWindow"); } const applet = await applets .connect(frame.contentWindow, { context: opts.context, }) .catch((err) => { console.error("Error connecting to " + src, err); throw err; }); if (opts.setHeight) { applet.onresize = () => { frame.height = `${applet.height}px`; frame.classList.add("has-loaded"); }; } else { if (frame.contentDocument?.readyState === "complete") { frame.classList.add("has-loaded"); } frame.addEventListener("load", () => { frame.classList.add("has-loaded"); }); } return applet; } //////////////////////////////////////////// // 🔮 Reactive state management //////////////////////////////////////////// export function reactive( applet: Applet, dataFn: (data: D) => T, effectFn: (t: T) => void, ) { const [getter, setter] = signal(dataFn(applet.data)); effect(() => { effectFn(getter()); return undefined; }); applet.addEventListener("data", (event: AppletEvent) => { setter(dataFn(event.data)); }); } //////////////////////////////////////////// // 🛠️ //////////////////////////////////////////// export function addScope(astroScope: string, object: O): O { return { ...object, attrs: { ...((object as any).attrs || {}), [`data-astro-cid-${astroScope}`]: "", }, }; } export function comparable(value: unknown) { return xxh32(JSON.stringify(value)); } export function hs( tag: string, astroScope: string, props?: Record | Signal>, configure?: ElementConfigurator, ) { const propsWithScope = props && isSignal(props) ? () => addScope(astroScope, props()) : addScope(astroScope, props || {}); return h(tag, propsWithScope, configure); } export function isPrimitive(test: unknown) { return test !== Object(test); } export function waitUntilAppletIsReady(applet: Applet): Promise { return new Promise((resolve) => { if (applet.data?.ready === true) { resolve(); return; } const callback = (event: AppletEvent) => { if (event.data?.ready === true) { applet.removeEventListener("data", callback); resolve(); } }; applet.addEventListener("data", callback); }); }