Welcome

GitHub

Welcome to KeenSpace, Ada

A Teams-style workspace demo for keen_phoenix_svelte — a library that auto-mounts compiled Svelte 5 apps into Phoenix (LiveView and plain pages) as self-contained islands.

Each area is an autonomous Svelte app: configured, handed a connection, then fully self-owning. No ~V sigil, no server-rendered slots, no SSR — the opposite of interleaving Elixir and Svelte.

Three areas, three transports

Every island is a separate compiled bundle, mounted into a plain <div>. What differs is how each one talks to the server.

The app boundary

Every island's entry receives one object — that's the entire coupling to Phoenix:

(target, { props, context, live, api, channel, bus, el }) => handle
props

Small per-island config, rendered by <.svelte props={…}>.

context

Page-wide user, CSRF, tokens, api_base and socket — emitted once per page.

live

The LiveView bridge: pushEvent / handleEvent / upload. null on plain pages.

api

REST helper; attaches x-csrf-token + the session cookie.

channel

Promise-based Phoenix channel factory with auto cid correlation.

bus

Page-wide client-side event bus for island-to-island messaging — no server.

Any framework — same boundary

The three areas above are Svelte, but the mount contract is framework-neutral. Below are three more islands — Lit, React and vanilla JS — each mounted through the same <.app> component. There are no per-framework variants; the optional framework attribute is just an informational data-framework tag. Click them — they all reach the activity bus.

Each passes its own <:placeholder> shaped like the control it's loading — a pill for Kudos, four buttons for reactions, a bar for the ticker — instead of the server-wide skeleton. Throttle your network (DevTools → Slow 3G) and reload to watch them resolve.

<.app framework="lit">

A Lit web component (shadow-DOM styles).

<.app framework="react">

React 18 + hooks, JSX built by esbuild.

<.app framework="js">

Plain JavaScript — no framework at all.

Islands talking to each other — the event bus

live, api and channel all reach the server. The bus is different: a page-wide, client-side event bus so independent islands can coordinate without the server and without knowing about each other. It behaves identically with or without LiveView.

activity island

Mounted once in the shell. Knows nothing about the others — it just bus.on("activity", …) and shows a toast.

video-catalogue

On save →
bus.emit("activity", {title: "Saved…"})

calendar

On "Join online" →
bus.emit("activity", {title: "Joined meeting"})

Try it: save a video or join a meeting and watch the toast fire in the corner — three separate bundles, coordinating over the bus.

An island loaded from elsewhere

Islands don't have to live under /apps. The one below is a framework-free bundle registered in config; the server emits a name → url manifest and the client imports it from there. A config toggle picks the mode of operation:

  • :direct — the browser imports the CDN URL itself (needs CORS + a permissive CSP).
  • :proxy — the browser imports a same-origin path and Phoenix fetches the bundle upstream (KeenPhoenixSvelte.Apps.Proxy) — no CORS, CSP 'self', the corporate-friendly mode. Active here in dev.

Works without LiveView, too

The very same island runs on a plain, controller-rendered page via mountStatic() with live: null — proof the apps don't depend on LiveView. See the plain calendar page, where the agenda and meeting chat work unchanged.