webtile-view#
A <webtile-view> custom element that renders an AT Protocol record through the Webtile
declared by an es.webtil.eView record.
It is a single self-contained module: its only runtime dependency
(json-p3, an RFC 9535 JSONPath engine used to evaluate
the eView's where clauses) is bundled into the build — there is no CDN import and
nothing to externalise.
Usage#
<webtile-view tile="at://did:plc:…/es.webtil.eView/games.gamesgamesgamesgames.actor.play">
</webtile-view>
<script type="module">
import "webtile-view"; // registers the <webtile-view> element
const view = document.querySelector("webtile-view");
// Assign (or reassign) a record to render it. Equivalent to setting the `record` attribute.
view.record = "at://did:plc:…/games.gamesgamesgamesgames.actor.play/3mo46juxwi2pz";
view.addEventListener("webtilerender", (e) => console.log("rendered", e.detail.uri));
view.addEventListener("webtileerror", (e) => console.warn("rejected", e.detail.message));
</script>
Importing the module registers the element as a side effect. An optional record attribute
renders immediately on connect.
What happens on each record#
Given a record AT-URI, the element (per the project README's sketch):
- Refuses records whose collection isn't the NSID the tile declares (the eView record's rkey).
- Resolves the record's DID to a PDS via slingshot, then fetches the record from that PDS (identity and record lookups are cached).
- Refuses records excluded by the eView's
whereJSONPath clauses (all clauses must match). postMessage({ kind: "render", record, uri })into the tile's sandboxed iframe.
The tile pulls anything else it needs (linked records, the owner's handle) back over postMessage
as allowlisted XRPC reads — see below — so the host doesn't gather references up front.
Rejections at steps 1 and 3 surface as a webtileerror event and an inline message.
Brokered data access#
A WebTile is meant to be network-isolated (the tiles spec cuts a tile
off from the network beyond its own pre-declared dependencies), so it can't query atproto itself.
Instead the host brokers a small allowlist of XRPC reads: the tile posts a request whose nsid
and params mirror the XRPC call, and the element routes it to the repo's PDS (resolving the repo
param to a PDS via slingshot) and replies with the same id. All network access and caching stay on
the host side of the sandbox.
// tile → host — `nsid` + `params` mirror the XRPC call; the routed `repo` param names the PDS
{ "kind": "xrpc", "id": "…", "nsid": "com.atproto.repo.getRecord", "params": { "repo": "did:…", "collection": "…", "rkey": "…" } }
{ "kind": "xrpc", "id": "…", "nsid": "com.atproto.repo.describeRepo", "params": { "repo": "did:… or handle" } }
// host → tile — `data` is the endpoint's response verbatim
{ "kind": "xrpc:result", "id": "…", "data": /* e.g. { uri, cid, value } or { handle, did, didDoc, … } */ }
{ "kind": "xrpc:error", "id": "…", "message": "…" }
The allowlist (currently com.atproto.repo.getRecord and com.atproto.repo.describeRepo) lives in
webtile-view.ts as XRPC_ROUTES; extend it to expose more reads. A tile can
wrap the protocol in a few lines (the example tiles in this repo use exactly this):
const xrpc = (() => {
let seq = 0;
const pending = new Map();
addEventListener("message", ({ data }) => {
if (!data || typeof data !== "object" || !("id" in data)) return;
const p = pending.get(data.id);
if (!p) return;
pending.delete(data.id);
if (data.kind === "xrpc:result") p.resolve(data.data);
else if (data.kind === "xrpc:error") p.reject(new Error(data.message));
});
return (nsid, params) =>
new Promise((resolve, reject) => {
const id = `wt-${seq++}`;
pending.set(id, { resolve, reject });
parent.postMessage({ kind: "xrpc", id, nsid, params }, "*");
});
})();
// e.g. the owner's handle, given the render `uri`:
const { handle } = await xrpc("com.atproto.repo.describeRepo", { repo: uri.split("/")[2] });
Styling#
The element is display: block. The tile iframe height is set with the --webtile-height custom
property (default 160px), and the status line / iframe are exposed as the status and frame
shadow parts. The iframe background is transparent, so each tile should paint its own
background (on html, so it fills the frame at any size).
Auto-height (autoheight)#
For tiles that should size themselves (e.g. full-page tiles), add the autoheight attribute and
the element will grow the iframe to the tile's reported content height, so the host page scrolls
rather than the iframe. Height reporting uses the iframe-resizer v4
wire protocol:
- The host posts the init handshake
[iFrameSizer]<id>:0:false:false:32:false:true:0:bodyOffset:::0:false:child:scroll:falseto the iframe on load. - The tile reports with
[iFrameSizer]<id>:<height>:<width>:resize(drop in@iframe-resizer/child, or post it by hand), optionally prompting the host first with[iFrameResizerChild]Ready.
Without autoheight, reports are ignored and --webtile-height stays in charge.
Build#
vp install
vp pack # → dist/webtile-view.mjs (ESM), webtile-view.umd.js (UMD), webtile-view.d.mts
vp run typecheck
The build (vp pack, which runs tsdown) bundles json-p3 in, so the emitted files are standalone.