Experiment to rebuild Diffuse using web applets.

feat: Better applet connective tissue

+126 -41
+5
astro.config.mjs
··· 1 + import { defineConfig } from "astro/config"; 2 + 3 + export default defineConfig({ 4 + trailingSlash: "always", 5 + });
+4 -3
deno.json
··· 1 1 { 2 2 "imports": { 3 - "@web-applets/sdk": "npm:@web-applets/sdk@0.2.0-alpha.8", 4 - "astro": "npm:astro@^5.4.1" 3 + "@web-applets/sdk": "npm:@web-applets/sdk@0.2.0-alpha.12", 4 + "astro": "npm:astro@^5.4.1", 5 + "spellcaster": "npm:spellcaster@^5.0.2" 5 6 }, 6 7 "tasks": { 7 8 "build": "astro build", 8 - "dev": "astro preview" 9 + "dev": "astro dev" 9 10 }, 10 11 "compilerOptions": { 11 12 "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
+17 -6
deno.lock
··· 1 1 { 2 2 "version": "4", 3 3 "specifiers": { 4 - "npm:@web-applets/sdk@0.2.0-alpha.8": "0.2.0-alpha.8", 4 + "npm:@web-applets/sdk@0.2.0-alpha.12": "0.2.0-alpha.12", 5 5 "npm:asto@*": "0.1.0", 6 6 "npm:asto@0.1.0": "0.1.0", 7 7 "npm:astro@5.4.2": "5.4.2_vite@6.2.1_zod@3.24.2", 8 8 "npm:astro@^5.4.1": "5.4.2_vite@6.2.1_zod@3.24.2", 9 - "npm:create-astro@latest": "4.11.1" 9 + "npm:create-astro@latest": "4.11.1", 10 + "npm:spellcaster@^5.0.2": "5.0.2" 10 11 }, 11 12 "npm": { 12 13 "@astrojs/cli-kit@0.4.1": { ··· 484 485 "@ungap/structured-clone@1.3.0": { 485 486 "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" 486 487 }, 487 - "@web-applets/sdk@0.2.0-alpha.8": { 488 - "integrity": "sha512-rEAAUlWy9IN8lNh7SGbnOPVGEOhtC7rfsCHK8Sc/sNhqTWY1Vwwiq6tsb+O2xHjibfWq3aJAQ7G8roR1Rcmzlg==" 488 + "@web-applets/sdk@0.2.0-alpha.12": { 489 + "integrity": "sha512-LCGHB/7q3h5EZReZAK9zrrODBDsFbJaNXhEhGFF8ga2JKXKnBCkeF3fYg7tTVsIOLY52dG7JkUo2911hEABfhw==" 489 490 }, 490 491 "acorn@8.14.1": { 491 492 "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" ··· 1979 1980 "signal-exit@3.0.7": { 1980 1981 "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 1981 1982 }, 1983 + "signal-polyfill@0.1.2": { 1984 + "integrity": "sha512-HT9d+L9NMiTzMxb/tU2Baym6129ROyRETSjvchvSkQa7wN0+SrG/IUlsaBLqKn2c+4mlze6CgQBEvgBjxOpiaQ==" 1985 + }, 1982 1986 "simple-swizzle@0.2.2": { 1983 1987 "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 1984 1988 "dependencies": [ ··· 2003 2007 }, 2004 2008 "space-separated-tokens@2.0.2": { 2005 2009 "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" 2010 + }, 2011 + "spellcaster@5.0.2": { 2012 + "integrity": "sha512-suCHnQlCyXAV1OCrL0jEjE8lCK+f2bfmnvBfIqkG6Q3fNiQ7mAaeXtSyEKCI5p2ifSieC5bS/59EcIfDh5PWMA==", 2013 + "dependencies": [ 2014 + "signal-polyfill" 2015 + ] 2006 2016 }, 2007 2017 "sprintf-js@1.0.3": { 2008 2018 "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" ··· 2308 2318 }, 2309 2319 "workspace": { 2310 2320 "dependencies": [ 2311 - "npm:@web-applets/sdk@0.2.0-alpha.8", 2312 - "npm:astro@^5.4.1" 2321 + "npm:@web-applets/sdk@0.2.0-alpha.12", 2322 + "npm:astro@^5.4.1", 2323 + "npm:spellcaster@^5.0.2" 2313 2324 ] 2314 2325 } 2315 2326 }
+26 -5
src/pages/engines/audio/index.astro
··· 12 12 <script> 13 13 import { applets } from "@web-applets/sdk"; 14 14 15 - const context = applets.register(); 15 + interface State { 16 + isPlaying: boolean; 17 + progress: number; 18 + } 19 + 20 + const context = applets.register<State>(); 16 21 const container = document.querySelector("#container"); 17 22 const audio = document.querySelector("audio"); 18 23 24 + //////////////////////////////////////////// 25 + // Initial state 26 + //////////////////////////////////////////// 27 + context.data = { 28 + isPlaying: false, 29 + progress: 0, 30 + }; 31 + 32 + //////////////////////////////////////////// 33 + // Audio events 34 + //////////////////////////////////////////// 19 35 audio.ontimeupdate = (event) => { 20 - context.data = { 21 - progress: 22 - isNaN(audio.duration) || audio.duration === 0 ? 0 : audio.currentTime / audio.duration, 23 - }; 36 + const progress = 37 + isNaN(audio.duration) || audio.duration === 0 ? 0 : audio.currentTime / audio.duration; 38 + context.data = { ...context.data, progress }; 24 39 }; 25 40 41 + audio.onpause = () => (context.data = { ...context.data, isPlaying: false }); 42 + audio.onplay = () => (context.data = { ...context.data, isPlaying: true }); 43 + 44 + //////////////////////////////////////////// 45 + // Actions 46 + //////////////////////////////////////////// 26 47 context.setActionHandler("load", (src: string) => { 27 48 audio.src = src; 28 49 });
+52 -22
src/pages/index.astro
··· 3 3 --- 4 4 5 5 <Page title="Diffuse Applets Usage Example"> 6 - <iframe src="engines/audio" frameborder="0" height="0" width="0"></iframe> 7 - <iframe src="interface/audio" frameborder="0" style="width: 100%"></iframe> 6 + <iframe src="engines/audio/" frameborder="0" height="0" width="0"></iframe> 7 + <iframe src="interface/audio/" frameborder="0" style="width: 100%"></iframe> 8 8 </Page> 9 + 10 + <style> 11 + body { 12 + background: darkgray; 13 + } 14 + </style> 9 15 10 16 <script> 11 17 import { applets } from "@web-applets/sdk"; 18 + import { effect, signal } from "spellcaster/spellcaster.js"; 12 19 13 - async function applet(src: string, opts: { setHeight?: boolean } = {}) { 14 - const frame: HTMLIFrameElement = document.querySelector(`[src="${src}"]`); 15 - 16 - if (opts.setHeight) { 17 - frame.addEventListener( 18 - "load", 19 - () => { 20 - frame.height = frame.contentWindow.document.body.scrollHeight + "px"; 21 - }, 22 - { once: true }, 23 - ); 24 - } 25 - 26 - return await applets.connect(frame.contentWindow); 27 - } 28 - 20 + //////////////////////////////////////////// 21 + // 🗂️ Applet index 22 + //////////////////////////////////////////// 29 23 const index = { 30 24 engines: { 31 25 audio: await applet("engines/audio"), ··· 35 29 }, 36 30 }; 37 31 38 - // Connect the applets 32 + //////////////////////////////////////////// 33 + // 🔮 Signals & effects 34 + //////////////////////////////////////////// 35 + const [progress, setProgress] = signal(0); 36 + const [isPlaying, setIsPlaying] = signal(false); 37 + 38 + effect(() => index.interface.audio.sendAction("set_is_playing", isPlaying())); 39 + effect(() => index.interface.audio.sendAction("set_progress", progress())); 40 + 41 + //////////////////////////////////////////// 42 + // ⚡ Connect applets 43 + //////////////////////////////////////////// 44 + index.engines.audio.ondata = (event) => { 45 + // NOTE: Instead of sending actions to the audio interface every time 46 + // this event is dispatched, which is at least every second when 47 + // audio is playing, we use signals and effects to manage that. 48 + setIsPlaying(event.data.isPlaying); 49 + setProgress(event.data.progress); 50 + }; 51 + 39 52 index.interface.audio.ondata = (event) => { 40 53 if (event.data.isPlaying) { 41 54 index.engines.audio.sendAction("play", null); ··· 44 57 } 45 58 }; 46 59 47 - index.engines.audio.ondata = (event) => { 48 - index.interface.audio.sendAction("set_progress", event.data.progress); 49 - }; 60 + //////////////////////////////////////////// 61 + // 🛠️ Applet initialiser 62 + //////////////////////////////////////////// 63 + async function applet(src: string, opts: { setHeight?: boolean } = {}) { 64 + const frame: HTMLIFrameElement = document.querySelector( 65 + `[src="${src}${src.endsWith("/") ? "" : "/"}"]`, 66 + ); 67 + 68 + if (opts.setHeight) { 69 + frame.addEventListener( 70 + "load", 71 + () => { 72 + frame.height = frame.contentWindow.document.body.scrollHeight + "px"; 73 + }, 74 + { once: true }, 75 + ); 76 + } 77 + 78 + return await applets.connect(frame.contentWindow); 79 + } 50 80 </script>
+22 -5
src/pages/interface/audio/index.astro
··· 19 19 20 20 const context = applets.register<State>(); 21 21 22 + //////////////////////////////////////////// 23 + // Initial state 24 + //////////////////////////////////////////// 25 + context.data = { 26 + isPlaying: false, 27 + }; 28 + 29 + //////////////////////////////////////////// 30 + // Actions 31 + //////////////////////////////////////////// 22 32 context.setActionHandler("set_is_playing", (isPlaying: boolean) => { 23 - context.data = { ...context.data, isPlaying }; 33 + context.data.isPlaying = isPlaying; 34 + render(); 24 35 }); 25 36 26 37 context.setActionHandler("set_progress", (progress: number) => { 27 38 document.body.querySelector("progress").value = Math.round(progress * 100); 39 + render(); 28 40 }); 29 41 30 - context.ondata = () => { 31 - document.body.querySelector("button").innerText = context.data.isPlaying ? "⏸️" : "▶️"; 32 - }; 33 - 42 + //////////////////////////////////////////// 43 + // DOM 44 + //////////////////////////////////////////// 34 45 document.body.querySelector("button").onclick = () => { 35 46 context.data = { isPlaying: !(context.data?.isPlaying ?? false) }; 36 47 }; 48 + 49 + function render() { 50 + document.body.querySelector("button").innerText = context.data.isPlaying ? "⏸️" : "▶️"; 51 + } 52 + 53 + render(); 37 54 </script> 38 55 </Applet>