[READ-ONLY] Mirror of https://github.com/jphastings/webtiles.byjp.me. A proof-of-concept for Atproto powered Webtile views.
0

Configure Feed

Select the types of activity you want to include in your feed.

webtiles.byjp.me / packages / webtile-view
1 folder 5 files
README.md

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):

  1. Refuses records whose collection isn't the NSID the tile declares (the eView record's rkey).
  2. Resolves the record's DID to a PDS via slingshot, then fetches the record from that PDS (identity and record lookups are cached).
  3. Refuses records excluded by the eView's where JSONPath clauses (all clauses must match).
  4. 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:false to 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.