Let the embedding page choose the embed's theme

4aa121d · Gabriel Morell · 2026-10-13 17:17

10 files +46 -2
Message
{commit_body(@commit)}

Files changed

modified README.md
+4 −1
@@ -18,7 +18,10 @@ site. Phoenix LiveView on top of PostgreSQL, with live multi-user editing.
18 18 - **Embeds** — publish a canvas read-only via `/embed/:token` (and the
19 19 `/c-embed/:slug` redirect for in-canvas links). Add `?mode=frame` for a
20 20 static framing — no live cursors or identity chip, diagram scaled to fill
21 − the iframe — suited to slides and docs.
21 + the iframe — suited to slides and docs. `?theme=<name>` picks the palette;
22 + the embedding page can also switch it live by posting
23 + `{type: "diogramos:set-theme", theme}` to the iframe, which announces
24 + `{type: "diogramos:ready"}` to its parent once it's listening.
22 25 - **JSON export** — `/c/:slug/export.json` for any canvas you can see.
23 26 - **Accounts** — magic-link login by default, password optional, with
24 27 invite-gated registration (`/users/invites`).
modified lib/diogramos_web/live/canvas_live/embed.ex
+31 −1
@@ -31,6 +31,13 @@ defmodule DiogramosWeb.CanvasLive.Embed do
31 31 frame = params["mode"] == "frame"
32 32 show_cursors = canvas.embed_show_cursors and not frame
33 33
34 + # `?theme=` lets the embedding page pick the palette up front; it can
35 + # also switch later over postMessage (see the EmbedViewport hook).
36 + theme =
37 + if params["theme"] in Diogramos.Themes.all(),
38 + do: params["theme"],
39 + else: canvas.theme
40 +
34 41 identity = %{
35 42 key: "embed-" <> Diogramos.ULID.generate(),
36 43 name: CanvasPresence.random_animal_name(),
@@ -56,6 +63,7 @@ defmodule DiogramosWeb.CanvasLive.Embed do
56 63 |> assign(:topic, topic)
57 64 |> assign(:frame, frame)
58 65 |> assign(:show_cursors, show_cursors)
66 + |> assign(:theme, theme)
59 67 |> assign(:peers, [])
60 68 |> assign(:identity, identity)}
61 69 end
@@ -147,9 +155,13 @@ defmodule DiogramosWeb.CanvasLive.Embed do
147 155 |> assign(:palette, DiogramosWeb.CanvasPresence.palette())
148 156
149 157 ~H"""
158 + <%!-- The parent page may switch data-theme from the client, so
159 + patches leave it alone. --%>
150 160 <div
151 161 id="embed-root"
152 − data-theme={@canvas.theme}
162 + data-theme={@theme}
163 + data-themes={Enum.join(Diogramos.Themes.all(), " ")}
164 + phx-mounted={JS.ignore_attributes(["data-theme"])}
153 165 class="min-h-screen w-screen bg-base-100 text-base-content relative"
154 166 >
155 167 <svg
@@ -333,6 +345,23 @@ defmodule DiogramosWeb.CanvasLive.Embed do
333 345 this.viewport = this.el.querySelector("#embed-viewport")
334 346 this.content = this.el.querySelector("#embed-content")
335 347 this.lastFitKey = null
348 + this.root = document.getElementById("embed-root")
349 + this.themes = (this.root.dataset.themes || "").split(" ")
350 +
351 + // Theme handshake with the embedding page:
352 + // parent → embed {type: "diogramos:set-theme", theme: "forest"}
353 + // embed → parent {type: "diogramos:ready"} once listening
354 + // Any origin may ask; the worst it can do is repaint the embed
355 + // with another of our own themes.
356 + this.onMessage = (e) => {
357 + const msg = e.data
358 + if (!msg || msg.type !== "diogramos:set-theme") return
359 + if (this.themes.includes(msg.theme)) this.root.setAttribute("data-theme", msg.theme)
360 + }
361 + window.addEventListener("message", this.onMessage)
362 + if (window.parent !== window) {
363 + window.parent.postMessage({ type: "diogramos:ready" }, "*")
364 + }
336 365 this.lastCursorAt = 0
337 366 this.scheduleFit()
338 367
@@ -371,6 +400,7 @@ defmodule DiogramosWeb.CanvasLive.Embed do
371 400 },
372 401 destroyed() {
373 402 window.removeEventListener("resize", this.onResize)
403 + window.removeEventListener("message", this.onMessage)
374 404 this.el.removeEventListener("pointermove", this.onPointerMove)
375 405 this.resizeObserver && this.resizeObserver.disconnect()
376 406 },
modified test/diogramos_web/live/canvas_embed_test.exs
+11 −0
@@ -172,6 +172,17 @@ defmodule DiogramosWeb.CanvasEmbedTest do
172 172 assert after_content =~ ~s(id="embed-cursors")
173 173 end
174 174
175 + test "?theme= overrides the canvas theme when it names a known theme", %{
176 + conn: conn,
177 + canvas: canvas
178 + } do
179 + {:ok, _lv, html} = live(conn, ~p"/embed/#{canvas.embed_token}?theme=forest")
180 + assert html =~ ~s(id="embed-root" data-theme="forest")
181 +
182 + {:ok, _lv, html} = live(conn, ~p"/embed/#{canvas.embed_token}?theme=nope")
183 + assert html =~ ~s(id="embed-root" data-theme="#{canvas.theme}")
184 + end
185 +
175 186 test "embed receives live updates from the editor", %{conn: conn, canvas: canvas, owner: owner} do
176 187 {:ok, embed_lv, _} = live(conn, ~p"/embed/#{canvas.embed_token}")
177 188

Parents: 2f6152a