atproto-embed 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,596 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ var API_BASE = "https://public.api.bsky.app/xrpc/";
5
+ var HANDLE_CACHE = new Map();
6
+ var PROFILE_CACHE = new Map();
7
+ var INFLIGHT = new Map();
8
+ var CONTAINER_ABORTS = new WeakMap();
9
+ var SIGNAL_IDS = new WeakMap();
10
+ var NEXT_SIGNAL_ID = 1;
11
+
12
+ function getSignalId(signal) {
13
+ if (!signal) return 0;
14
+ var id = SIGNAL_IDS.get(signal);
15
+ if (!id) {
16
+ id = NEXT_SIGNAL_ID++;
17
+ SIGNAL_IDS.set(signal, id);
18
+ }
19
+ return id;
20
+ }
21
+
22
+ function fetchJsonOrThrow(url, errMsg, signal) {
23
+ return fetch(url, signal ? { signal: signal } : undefined).then(function (res) {
24
+ if (!res.ok) throw new Error(errMsg);
25
+ return res.json();
26
+ });
27
+ }
28
+
29
+ function fetchJsonDedup(key, url, errMsg, signal) {
30
+ var sigId = getSignalId(signal);
31
+ var inflightKey = key + "|s" + sigId;
32
+ if (INFLIGHT.has(inflightKey)) return INFLIGHT.get(inflightKey);
33
+ var p = fetchJsonOrThrow(url, errMsg, signal).finally(function () {
34
+ INFLIGHT.delete(inflightKey);
35
+ });
36
+ INFLIGHT.set(inflightKey, p);
37
+ return p;
38
+ }
39
+
40
+ function resolveHandle(handle) {
41
+ if (!handle) return Promise.resolve(null);
42
+ if (handle.startsWith("did:")) return Promise.resolve(handle);
43
+ if (HANDLE_CACHE.has(handle)) return HANDLE_CACHE.get(handle);
44
+ var url =
45
+ API_BASE +
46
+ "com.atproto.identity.resolveHandle?handle=" +
47
+ encodeURIComponent(handle);
48
+ var p = fetchJsonDedup(url, url, "Handle resolution failed", null)
49
+ .then(function (data) {
50
+ return data.did;
51
+ })
52
+ .catch(function (err) {
53
+ HANDLE_CACHE.delete(handle);
54
+ throw err;
55
+ });
56
+ HANDLE_CACHE.set(handle, p);
57
+ return p;
58
+ }
59
+
60
+ function fetchProfile(actor, signal) {
61
+ if (!actor) return Promise.resolve(null);
62
+ if (PROFILE_CACHE.has(actor)) {
63
+ return Promise.resolve(PROFILE_CACHE.get(actor));
64
+ }
65
+ var url =
66
+ API_BASE +
67
+ "app.bsky.actor.getProfile?actor=" +
68
+ encodeURIComponent(actor);
69
+ return fetchJsonDedup("profile:" + actor, url, "Failed to fetch profile", signal)
70
+ .then(function (data) {
71
+ PROFILE_CACHE.set(actor, data);
72
+ return data;
73
+ })
74
+ .catch(function (err) {
75
+ if (err && err.name === "AbortError") throw err;
76
+ return null;
77
+ });
78
+ }
79
+
80
+ function el(tag, className, attrs) {
81
+ var e = document.createElement(tag);
82
+ if (className) e.className = className;
83
+ if (attrs) {
84
+ for (var k in attrs) {
85
+ if (k === "textContent") e.textContent = attrs[k];
86
+ else if (k === "innerHTML") e.innerHTML = attrs[k];
87
+ else e.setAttribute(k, attrs[k]);
88
+ }
89
+ }
90
+ return e;
91
+ }
92
+
93
+ function formatCount(n) {
94
+ if (n >= 1000000)
95
+ return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
96
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "K";
97
+ return String(n);
98
+ }
99
+
100
+ var ICONS = {
101
+ "avatar-fallback": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2248%22%20height%3D%2248%22%20fill%3D%22none%22%20viewBox%3D%220%200%2048%2048%22%3E%3Crect%20width%3D%2248%22%20height%3D%2248%22%20fill%3D%22%23e0e0e0%22%20rx%3D%2224%22%2F%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M32%2020a8%208%200%201%201-16%200%208%208%200%200%201%2016%200%22%20opacity%3D%22.2%22%2F%3E%3Cpath%20fill%3D%22%23000%22%20d%3D%22M36.865%2034.5c-1.904-3.291-4.838-5.651-8.261-6.77a9%209%200%201%200-9.208%200c-3.424%201.117-6.357%203.477-8.261%206.77a.997.997%200%200%200%20.352%201.389%201%201%200%200%200%201.38-.389C15.22%2031.43%2019.383%2029%2024%2029s8.779%202.43%2011.134%206.5a1.001%201.001%200%201%200%201.73-1M17%2020a7%207%200%201%201%207%207%207.007%207.007%200%200%201-7-7%22%2F%3E%3C%2Fsvg%3E",
102
+ "check-circle": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M16%203a13%2013%200%201%200%2013%2013A13.013%2013.013%200%200%200%2016%203m5.708%2010.708-7%207a1%201%200%200%201-1.415%200l-3-3a1%201%200%200%201%201.415-1.415L14%2018.586l6.293-6.293a1%201%200%200%201%201.415%201.415%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%223%22%20x2%3D%2229%22%20y1%3D%223%22%20y2%3D%2229%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23006aff%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23004099%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
103
+ "original-seal": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M28.233%2012.853c-.472-.493-.96-1-1.143-1.447-.17-.409-.18-1.086-.19-1.742-.019-1.22-.039-2.603-1-3.564s-2.344-.981-3.564-1c-.656-.01-1.333-.02-1.742-.19-.445-.184-.954-.671-1.447-1.142C18.286%202.938%2017.306%202%2016%202s-2.284.939-3.148%201.768c-.492.47-1%20.958-1.446%201.142-.406.17-1.086.18-1.742.19-1.22.019-2.603.039-3.564%201s-.975%202.344-1%203.564c-.01.656-.02%201.334-.19%201.742-.184.445-.671.954-1.142%201.446C2.938%2013.715%202%2014.696%202%2016s.939%202.284%201.768%203.148c.47.492.958%201%201.142%201.446.17.409.18%201.086.19%201.742.019%201.22.039%202.603%201%203.564s2.344.981%203.564%201c.656.01%201.334.02%201.742.19.445.184.954.671%201.446%201.143C13.715%2029.06%2014.696%2030%2016%2030s2.284-.939%203.148-1.767c.492-.472%201-.96%201.446-1.143.409-.17%201.086-.18%201.742-.19%201.22-.019%202.603-.039%203.564-1s.981-2.344%201-3.564c.01-.656.02-1.333.19-1.742.184-.445.671-.954%201.143-1.447C29.06%2018.286%2030%2017.306%2030%2016s-.939-2.284-1.767-3.148m-6.526.854-7%207a1%201%200%200%201-1.415%200l-3-3a1%201%200%200%201%201.415-1.415L14%2018.587l6.293-6.293a1%201%200%200%201%201.415%201.415%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%222%22%20x2%3D%2230%22%20y1%3D%222%22%20y2%3D%2230%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23ff6200%22%2F%3E%3Cstop%20offset%3D%22.615%22%20stop-color%3D%22%23f80%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23ff5900%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E",
104
+ "trusted-seal": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22none%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22url(%23a)%22%20d%3D%22M28.233%2012.853c-.472-.493-.96-1-1.143-1.447-.17-.409-.18-1.086-.19-1.742-.019-1.22-.039-2.603-1-3.564s-2.344-.981-3.564-1c-.656-.01-1.333-.02-1.742-.19-.445-.184-.954-.671-1.447-1.142C18.286%202.938%2017.306%202%2016%202s-2.284.939-3.148%201.768c-.492.47-1%20.958-1.446%201.142-.406.17-1.086.18-1.742.19-1.22.019-2.603.039-3.564%201s-.975%202.344-1%203.564c-.01.656-.02%201.334-.19%201.742-.184.445-.671.954-1.142%201.446C2.938%2013.715%202%2014.696%202%2016s.939%202.284%201.768%203.148c.47.492.958%201%201.142%201.446.17.409.18%201.086.19%201.742.019%201.22.039%202.603%201%203.564s2.344.981%203.564%201c.656.01%201.334.02%201.742.19.445.184.954.671%201.446%201.143C13.715%2029.06%2014.696%2030%2016%2030s2.284-.939%203.148-1.767c.492-.472%201-.96%201.446-1.143.409-.17%201.086-.18%201.742-.19%201.22-.019%202.603-.039%203.564-1s.981-2.344%201-3.564c.01-.656.02-1.333.19-1.742.184-.445.671-.954%201.143-1.447C29.06%2018.286%2030%2017.306%2030%2016s-.939-2.284-1.767-3.148m-6.526.854-7%207a1%201%200%200%201-1.415%200l-3-3a1%201%200%200%201%201.415-1.415L14%2018.587l6.293-6.293a1%201%200%200%201%201.415%201.415%22%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22a%22%20x1%3D%222%22%20x2%3D%2230%22%20y1%3D%222%22%20y2%3D%2230%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%2329de6b%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%230f9f44%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3C%2Fsvg%3E"
105
+ };
106
+
107
+ function getIconUrl(name) {
108
+ return ICONS[name] || "";
109
+ }
110
+
111
+ var BADGE_ICONS = {
112
+ crown: "original-seal",
113
+ seal: "trusted-seal",
114
+ check: "check-circle",
115
+ };
116
+
117
+ function detectBadge(author) {
118
+ var v = (author && author.verification) || {};
119
+ if (v.trustedVerifierStatus === "valid") {
120
+ return author && author.handle === "bsky.app" ? "crown" : "seal";
121
+ }
122
+ if (v.verifiedStatus === "valid") return "check";
123
+ return null;
124
+ }
125
+
126
+ function renderBadgeIcon(author) {
127
+ var badge = detectBadge(author);
128
+ if (!badge || !BADGE_ICONS[badge]) return null;
129
+ return el("img", "atproto-members__badge", {
130
+ src: getIconUrl(BADGE_ICONS[badge]),
131
+ alt: badge,
132
+ width: "14",
133
+ height: "14",
134
+ });
135
+ }
136
+
137
+ /* ───── List Parsing ───── */
138
+
139
+ function parseListInput(raw) {
140
+ if (!raw) return null;
141
+ raw = raw.trim();
142
+ if (raw.startsWith("at://")) {
143
+ if (raw.indexOf("/app.bsky.graph.starterpack/") !== -1) {
144
+ return { starterPackAtUri: raw };
145
+ }
146
+ return { atUri: raw };
147
+ }
148
+ var m = raw.match(/https?:\/\/([^/]+)\/profile\/([^/]+)\/lists\/([a-zA-Z0-9]+)/);
149
+ if (m) return { handle: m[2], rkey: m[3] };
150
+ var sp = raw.match(/https?:\/\/([^/]+)\/profile\/([^/]+)\/starter-pack\/([a-zA-Z0-9]+)/);
151
+ if (sp) return { starterPackHandle: sp[2], starterPackRkey: sp[3] };
152
+ var sp2 = raw.match(/https?:\/\/([^/]+)\/starter-pack\/([^/]+)\/([a-zA-Z0-9]+)/);
153
+ if (sp2) return { starterPackHandle: sp2[2], starterPackRkey: sp2[3], starterPackDomain: sp2[1] };
154
+ return null;
155
+ }
156
+
157
+ async function resolveListUri(raw, config) {
158
+ var parsed = parseListInput(raw);
159
+ if (!parsed) throw new Error("Invalid list");
160
+ if (parsed.atUri) return { listUri: parsed.atUri, starterPackUri: null };
161
+ if (parsed.starterPackAtUri) return { listUri: null, starterPackUri: parsed.starterPackAtUri };
162
+ if (parsed.starterPackHandle) {
163
+ if (config && parsed.starterPackDomain) {
164
+ config.clientDomain = config.clientDomain || parsed.starterPackDomain;
165
+ }
166
+ var spDid = await resolveHandle(parsed.starterPackHandle);
167
+ return {
168
+ listUri: null,
169
+ starterPackUri:
170
+ "at://" + spDid + "/app.bsky.graph.starterpack/" + parsed.starterPackRkey,
171
+ };
172
+ }
173
+ var did = await resolveHandle(parsed.handle);
174
+ return {
175
+ listUri: "at://" + did + "/app.bsky.graph.list/" + parsed.rkey,
176
+ starterPackUri: null,
177
+ };
178
+ }
179
+
180
+ async function fetchList(atUri, limit, signal) {
181
+ var url =
182
+ API_BASE +
183
+ "app.bsky.graph.getList?list=" +
184
+ encodeURIComponent(atUri);
185
+ if (limit) url += "&limit=" + encodeURIComponent(String(limit));
186
+ return fetchJsonDedup(url, url, "Failed to fetch list", signal);
187
+ }
188
+
189
+ async function fetchStarterPack(atUri, signal) {
190
+ var url =
191
+ API_BASE +
192
+ "app.bsky.graph.getStarterPack?starterPack=" +
193
+ encodeURIComponent(atUri);
194
+ return fetchJsonDedup(url, url, "Failed to fetch starter pack", signal);
195
+ }
196
+
197
+ /* ───── Config ───── */
198
+
199
+ function parseBoolAttr(container, name, defaultVal) {
200
+ var val = container.getAttribute("data-" + name);
201
+ if (val === null) return defaultVal;
202
+ return val === "true" || val === "1" || val === "";
203
+ }
204
+
205
+ function parseStringAttr(container, name, defaultVal) {
206
+ var val = container.getAttribute("data-" + name);
207
+ return val !== null ? val : defaultVal;
208
+ }
209
+
210
+ function parseConfig(container) {
211
+ var config = {
212
+ showAvatar: true,
213
+ showDisplayName: true,
214
+ showHandle: true,
215
+ showVerification: true,
216
+ showMetrics: true,
217
+ showFollowers: true,
218
+ showFollowing: true,
219
+ showButton: true,
220
+ buttonStyle: "filled",
221
+ buttonLabel: "View more",
222
+ listUrl: null,
223
+ clientBase: null,
224
+ clientDomain: null,
225
+ columns: "3",
226
+ limit: 16,
227
+ width: null,
228
+ maxWidth: null,
229
+ };
230
+
231
+ config.showAvatar = parseBoolAttr(container, "show-avatar", config.showAvatar);
232
+ config.showDisplayName = parseBoolAttr(container, "show-display-name", config.showDisplayName);
233
+ config.showHandle = parseBoolAttr(container, "show-handle", config.showHandle);
234
+ config.showVerification = parseBoolAttr(container, "show-verification", config.showVerification);
235
+ config.showMetrics = parseBoolAttr(container, "show-metrics", config.showMetrics);
236
+ config.showFollowers = parseBoolAttr(container, "show-followers", config.showFollowers);
237
+ config.showFollowing = parseBoolAttr(container, "show-following", config.showFollowing);
238
+ config.showPosts = parseBoolAttr(container, "show-posts", config.showPosts);
239
+ config.showButton = parseBoolAttr(container, "show-button", config.showButton);
240
+
241
+ config.columns = parseStringAttr(container, "columns", config.columns);
242
+ if (config.columns === "true") config.columns = "2";
243
+ if (config.columns === "false") config.columns = "3";
244
+
245
+ var lim = parseInt(parseStringAttr(container, "limit", ""), 10);
246
+ if (isFinite(lim) && lim > 0) config.limit = lim;
247
+
248
+ config.width = parseStringAttr(container, "width", config.width);
249
+ config.maxWidth = parseStringAttr(container, "max-width", config.maxWidth);
250
+ config.buttonStyle = parseStringAttr(container, "button-style", config.buttonStyle);
251
+ config.buttonLabel = parseStringAttr(container, "button-label", config.buttonLabel);
252
+ config.listUrl = parseStringAttr(container, "list-url", config.listUrl);
253
+ config.clientBase = parseStringAttr(container, "client-base", config.clientBase);
254
+ config.clientDomain = parseStringAttr(container, "client-domain", config.clientDomain);
255
+
256
+ return config;
257
+ }
258
+
259
+ function applyLayoutVars(container, config) {
260
+ if (!config) return;
261
+ if (config.columns) {
262
+ var parts = String(config.columns).split(",").map(function (p) {
263
+ return p.trim();
264
+ }).filter(Boolean);
265
+ var colsLg = parts[0] || "3";
266
+ var colsMd = parts[1] || colsLg;
267
+ var colsSm = parts[2] || colsMd;
268
+ var colsXs = parts[3] || colsSm;
269
+ container.style.setProperty("--atproto-columns", colsLg);
270
+ container.style.setProperty("--atproto-columns-md", colsMd);
271
+ container.style.setProperty("--atproto-columns-sm", colsSm);
272
+ container.style.setProperty("--atproto-columns-xs", colsXs);
273
+ }
274
+ if (config.width) container.style.setProperty("--atproto-width", config.width);
275
+ if (config.maxWidth) container.style.setProperty("--atproto-max-width", config.maxWidth);
276
+ }
277
+
278
+ function extractClientDomain(raw) {
279
+ if (!raw || raw.indexOf("http") !== 0) return null;
280
+ try {
281
+ return new URL(raw).hostname;
282
+ } catch (_) {
283
+ return null;
284
+ }
285
+ }
286
+
287
+ function clientBase(config) {
288
+ if (config.clientBase) {
289
+ if (config.clientBase.indexOf("http") === 0) return config.clientBase;
290
+ return "https://" + config.clientBase;
291
+ }
292
+ if (config.clientDomain) return "https://" + config.clientDomain;
293
+ return "https://bsky.app";
294
+ }
295
+
296
+ function profileUrl(config, handleOrDid) {
297
+ return clientBase(config) + "/profile/" + handleOrDid;
298
+ }
299
+
300
+ function listUrlFromAtUri(atUri, config) {
301
+ if (!atUri || atUri.indexOf("at://") !== 0) return null;
302
+ var m = atUri.match(/^at:\/\/([^/]+)\/app\.bsky\.graph\.list\/([^/]+)$/);
303
+ if (!m) return null;
304
+ return clientBase(config) + "/profile/" + m[1] + "/lists/" + m[2];
305
+ }
306
+
307
+ function renderMemberCard(profile, config) {
308
+ if (!profile) return null;
309
+ var card = el("div", "atproto-members__card");
310
+
311
+ if (config.showAvatar !== false) {
312
+ card.appendChild(
313
+ el("img", "atproto-members__avatar", {
314
+ src: profile.avatar || getIconUrl("avatar-fallback"),
315
+ alt: profile.displayName || profile.handle || "Avatar",
316
+ loading: "lazy",
317
+ })
318
+ );
319
+ }
320
+
321
+ var meta = el("div", "atproto-members__meta");
322
+ if (config.showDisplayName !== false) {
323
+ var nameRow = el("div", "atproto-members__name-row");
324
+ nameRow.appendChild(
325
+ el("span", "atproto-members__name", {
326
+ textContent: profile.displayName || profile.handle || "Member",
327
+ })
328
+ );
329
+ if (config.showVerification !== false) {
330
+ var badge = renderBadgeIcon(profile);
331
+ if (badge) nameRow.appendChild(badge);
332
+ }
333
+ meta.appendChild(nameRow);
334
+ }
335
+ if (config.showHandle !== false && profile.handle) {
336
+ meta.appendChild(
337
+ el("div", "atproto-members__handle", {
338
+ textContent:
339
+ profile.handle.charAt(0) === "@"
340
+ ? profile.handle
341
+ : "@" + profile.handle,
342
+ })
343
+ );
344
+ }
345
+
346
+ if (config.showMetrics !== false) {
347
+ var metrics = el("div", "atproto-members__metrics");
348
+ if (config.showPosts !== false) {
349
+ metrics.appendChild(
350
+ el("span", "atproto-members__metric", {
351
+ textContent: formatCount(profile.postsCount || 0) + " posts",
352
+ })
353
+ );
354
+ }
355
+ if (config.showFollowers !== false) {
356
+ metrics.appendChild(
357
+ el("span", "atproto-members__metric", {
358
+ textContent: formatCount(profile.followersCount || 0) + " followers",
359
+ })
360
+ );
361
+ }
362
+ if (config.showFollowing !== false) {
363
+ metrics.appendChild(
364
+ el("span", "atproto-members__metric", {
365
+ textContent: formatCount(profile.followsCount || 0) + " following",
366
+ })
367
+ );
368
+ }
369
+ meta.appendChild(metrics);
370
+ }
371
+
372
+ if (config.showAvatar !== false) {
373
+ var onlyAvatar =
374
+ config.showDisplayName === false &&
375
+ (config.showHandle === false || !profile.handle) &&
376
+ config.showMetrics === false;
377
+ if (onlyAvatar) {
378
+ card.classList.add("atproto-members__card--avatar-only");
379
+ }
380
+ }
381
+
382
+ card.appendChild(meta);
383
+
384
+ var handle = profile.handle || profile.did;
385
+ if (!handle) return card;
386
+ var link = el("a", "atproto-members__card-link", {
387
+ href: profileUrl(config, handle),
388
+ target: "_blank",
389
+ rel: "noopener noreferrer",
390
+ });
391
+ link.appendChild(card);
392
+ return link;
393
+ }
394
+
395
+ function renderMembers(listData, config, listLink) {
396
+ var wrapper = el("div", "atproto-members__wrap");
397
+ var grid = el("div", "atproto-members__grid");
398
+ var items = (listData && listData.items) || [];
399
+ if (!items.length) {
400
+ return el("div", "atproto-members__empty", {
401
+ textContent: "No members found",
402
+ });
403
+ }
404
+ var avatarOnly =
405
+ config.showAvatar !== false &&
406
+ config.showDisplayName === false &&
407
+ config.showHandle === false &&
408
+ config.showMetrics === false;
409
+ if (avatarOnly) {
410
+ grid.classList.add("atproto-members__grid--avatar-only");
411
+ }
412
+ if (items.length === 1) {
413
+ grid.style.gridTemplateColumns = "1fr";
414
+ } else if (items.length === 2) {
415
+ grid.style.gridTemplateColumns = "1fr 1fr";
416
+ }
417
+ items.forEach(function (item) {
418
+ var subject = item.subject || item.profile || item;
419
+ if (!subject) return;
420
+ grid.appendChild(renderMemberCard(subject, config));
421
+ });
422
+ wrapper.appendChild(grid);
423
+
424
+ if (config.showButton !== false && listLink) {
425
+ var row = el("div", "atproto-members__button-row");
426
+ var btnClass = "atproto-members__button";
427
+ if (config.buttonStyle === "outline") {
428
+ btnClass += " atproto-members__button--outline";
429
+ } else {
430
+ btnClass += " atproto-members__button--filled";
431
+ }
432
+ row.appendChild(
433
+ el("a", btnClass, {
434
+ href: listLink,
435
+ target: "_blank",
436
+ rel: "noopener noreferrer",
437
+ textContent: config.buttonLabel || "View more",
438
+ })
439
+ );
440
+ wrapper.appendChild(row);
441
+ }
442
+
443
+ return wrapper;
444
+ }
445
+
446
+ /* ───── CSS injection ───── */
447
+
448
+ function injectStyles(root) {
449
+ var style = document.createElement("style");
450
+ style.textContent = "@import url('https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&display=swap');:host{--neutral-0:#ffffff;--neutral-1:#f8f9fa;--neutral-2:#f1f3f5;--neutral-3:#e9ecef;--neutral-4:#dee2e6;--neutral-5:#ced4da;--neutral-6:#adb5bd;--neutral-7:#6a7178;--neutral-8:#4f575e;--neutral-9:#272b30;--neutral-10:#101213;--neutral-11:#000000;--primary-light:#f8f9ff;--primary-base:#0a66f4;--primary-hover:#20439b;--primary-dark:#1c2855;--font-displayLarge:45px;--font-displaymedium:40px;--font-displaySmall:36px;--font-heading1:32px;--font-heading2:28px;--font-heading3:25px;--font-heading4:22px;--font-heading5:20px;--font-heading6:18px;--font-subtitle:16px;--font-body:14px;--font-caption:12px;--font-label:11px;--font-tagline:10px;--font-sans:\"Google Sans Flex\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;--transition:all 0.32s ease-in-out;--atproto-columns:3;--atproto-bg:var(--neutral-0,#ffffff);--atproto-border-color:var(--neutral-3,#e9ecef);--atproto-text-color:var(--neutral-10,#101213);--atproto-muted-color:var(--neutral-7,#6a7178);--atproto-accent-color:var(--primary-base,#0a66f4);--atproto-radius:12px;--atproto-font-family:var(--font-sans,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif)}:host(.atproto-members-host){display:block;width:var(--atproto-width,100%);max-width:var(--atproto-max-width,none);margin:32px auto;font-family:var(--atproto-font-family);color:var(--atproto-text-color)}.atproto-members-inner{width:100%}.atproto-members--loading,.atproto-members--error,.atproto-members__empty{padding:18px;border:1px solid var(--atproto-border-color);border-radius:var(--atproto-radius);background:var(--atproto-bg);color:var(--atproto-muted-color);text-align:center;font-size:var(--font-body,14px)}.atproto-members__grid{display:grid;grid-template-columns:repeat(var(--atproto-columns,3),minmax(0,1fr));gap:8px}.atproto-members__grid--avatar-only{grid-template-columns:repeat(auto-fit,minmax(46px,1fr)) !important;justify-items:center}@media (max-width:1024px){.atproto-members__grid{grid-template-columns:repeat(var(--atproto-columns-md,var(--atproto-columns,3)),minmax(0,1fr))}.atproto-members__grid--avatar-only{grid-template-columns:repeat(auto-fit,minmax(46px,1fr))}}@media (max-width:768px){.atproto-members__grid{grid-template-columns:repeat(var(--atproto-columns-sm,var(--atproto-columns-md,var(--atproto-columns,3))),minmax(0,1fr))}.atproto-members__grid--avatar-only{grid-template-columns:repeat(auto-fit,minmax(46px,1fr))}}.atproto-members__button-row{display:flex;justify-content:center;margin-top:24px}.atproto-members__button{display:inline-flex;align-items:center;justify-content:center;padding:8px 20px;border-radius:100px;font-size:var(--font-body);font-weight:600;text-decoration:none;transition:transform 0.2s ease,box-shadow 0.2s ease,background 0.2s ease}.atproto-members__button--filled{background:var(--primary-base);color:#ffffff;border:1px solid var(--primary-base)}.atproto-members__button--filled:hover{background:var(--primary-hover)}.atproto-members__button--outline{background:transparent;color:var(--neutral-11);border:2px solid var(--neutral-1)}.atproto-members__button--outline:hover{background:var(--neutral-3);border-color:var(--neutral-3)}.atproto-members__card{display:flex;gap:10px;padding:12px;background:var(--neutral-0);border:2px solid var(--neutral-1);border-radius:12px;align-items:flex-start;transition:var(--transition)}.atproto-members__card--avatar-only{justify-content:center;align-items:center;padding:4px;border-radius:50%;width:46px;height:46px;gap:0;box-sizing:border-box}.atproto-members__card--avatar-only .atproto-members__meta{display:none}.atproto-members__card-link{display:block;text-decoration:none;color:inherit}.atproto-members__card-link:hover .atproto-members__card{border-color:var(--neutral-3);box-shadow:0 6px 16px rgba(0,0,0,0.08);transform:translateY(-1px)}.atproto-members__avatar{width:38px;height:38px;border-radius:50%;object-fit:cover;flex-shrink:0}.atproto-members__meta{display:flex;flex-direction:column;gap:4px;min-width:0}.atproto-members__name-row{display:flex;align-items:center;gap:4px}.atproto-members__name{font-size:var(--font-caption);font-weight:600;color:var(--neutral-11);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.atproto-members__badge{width:14px;height:14px}.atproto-members__handle{font-size:var(--font-caption);color:var(--neutral-8);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.atproto-members__metrics{display:flex;gap:8px;margin-top:2px}.atproto-members__metric{display:flex;align-items:center;gap:4px;font-size:var(--font-tagline);color:var(--neutral-7);white-space:nowrap}@media (max-width:480px){.atproto-members__grid{grid-template-columns:repeat(var(--atproto-columns-xs,var(--atproto-columns-sm,var(--atproto-columns,3))),minmax(0,1fr))}}";
451
+ root.appendChild(style);
452
+ }
453
+
454
+ function ensureFontLoaded() {
455
+ if (typeof document === "undefined") return;
456
+ if (document.getElementById("atproto-font-google-sans")) return;
457
+ var link = document.createElement("link");
458
+ link.id = "atproto-font-google-sans";
459
+ link.setAttribute("rel", "stylesheet");
460
+ link.href =
461
+ "https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&display=swap";
462
+ document.head.appendChild(link);
463
+ }
464
+
465
+ /* ───── Init ───── */
466
+
467
+ async function initContainer(container) {
468
+ var raw = container.getAttribute("data-list");
469
+ if (!raw) return;
470
+
471
+ var previous = CONTAINER_ABORTS.get(container);
472
+ if (previous) previous.abort();
473
+ var controller = new AbortController();
474
+ CONTAINER_ABORTS.set(container, controller);
475
+
476
+ var config = parseConfig(container);
477
+ if (!config.clientDomain) {
478
+ var inferred = extractClientDomain(raw);
479
+ if (inferred) config.clientDomain = inferred;
480
+ }
481
+ applyLayoutVars(container, config);
482
+
483
+ container.classList.add("atproto-members-host");
484
+
485
+ var shadow = container.shadowRoot;
486
+ if (!shadow) {
487
+ shadow = container.attachShadow({ mode: "open" });
488
+ } else {
489
+ shadow.innerHTML = "";
490
+ }
491
+
492
+ ensureFontLoaded();
493
+ injectStyles(shadow);
494
+
495
+ var wrapper = el("div", "atproto-members-inner");
496
+ shadow.appendChild(wrapper);
497
+ wrapper.appendChild(
498
+ el("div", "atproto-members--loading", { textContent: "Loading members…" })
499
+ );
500
+
501
+ try {
502
+ var listInfo = await resolveListUri(raw, config);
503
+ var listUri = listInfo.listUri;
504
+ var starterPackUri = listInfo.starterPackUri;
505
+ var listData;
506
+ if (starterPackUri) {
507
+ var starter = await fetchStarterPack(starterPackUri, controller.signal);
508
+ var spList = starter && starter.starterPack && starter.starterPack.record && starter.starterPack.record.list;
509
+ if (!spList) throw new Error("Starter pack missing list");
510
+ listUri = spList;
511
+ }
512
+ listData = await fetchList(listUri, config.limit, controller.signal);
513
+ if (listData && listData.items && config.limit && config.limit > 0) {
514
+ var validCount = listData.items.filter(function (item) {
515
+ return item && (item.subject || item.profile);
516
+ }).length;
517
+ if (validCount < config.limit && config.limit < 100) {
518
+ var extra = config.limit - validCount;
519
+ var nextLimit = Math.min(100, config.limit + extra);
520
+ if (nextLimit > config.limit) {
521
+ listData = await fetchList(listUri, nextLimit, controller.signal);
522
+ }
523
+ }
524
+ }
525
+ if (config.showMetrics !== false) {
526
+ var items = (listData && listData.items) || [];
527
+ var enrich = items.map(function (item) {
528
+ var subject = item.subject || item.profile || item;
529
+ if (!subject || !subject.did) return Promise.resolve(null);
530
+ return fetchProfile(subject.did, controller.signal).then(function (p) {
531
+ if (p) item.subject = p;
532
+ return p;
533
+ });
534
+ });
535
+ await Promise.all(enrich);
536
+ }
537
+ wrapper.innerHTML = "";
538
+ var listLink = null;
539
+ if (raw && raw.indexOf("http") === 0) listLink = raw;
540
+ if (!listLink && config.listUrl) listLink = config.listUrl;
541
+ if (!listLink && listUri) listLink = listUrlFromAtUri(listUri, config);
542
+ wrapper.appendChild(renderMembers(listData, config, listLink));
543
+ } catch (err) {
544
+ if (err && err.name === "AbortError") return;
545
+ wrapper.innerHTML = "";
546
+ wrapper.appendChild(
547
+ el("div", "atproto-members--error", {
548
+ textContent: "Failed to load members",
549
+ })
550
+ );
551
+ console.error("[atproto-members]", err);
552
+ }
553
+ }
554
+
555
+ function init(force) {
556
+ var containers = document.querySelectorAll(
557
+ ".atproto-members:not([data-members-child])"
558
+ );
559
+ containers.forEach(function (c) {
560
+ if (!force && c.getAttribute("data-loaded")) return;
561
+ var lazy = c.getAttribute("data-lazy");
562
+ if (lazy === "true" && typeof IntersectionObserver !== "undefined") {
563
+ if (c.getAttribute("data-loaded") === "pending") return;
564
+ c.setAttribute("data-loaded", "pending");
565
+ var observer = new IntersectionObserver(function (entries) {
566
+ entries.forEach(function (entry) {
567
+ if (!entry.isIntersecting) return;
568
+ observer.disconnect();
569
+ c.setAttribute("data-loaded", "true");
570
+ initContainer(c);
571
+ });
572
+ }, { rootMargin: "200px 0px" });
573
+ observer.observe(c);
574
+ } else {
575
+ c.setAttribute("data-loaded", "true");
576
+ initContainer(c);
577
+ }
578
+ });
579
+ }
580
+
581
+ if (document.readyState === "loading") {
582
+ document.addEventListener("DOMContentLoaded", init);
583
+ } else {
584
+ init();
585
+ }
586
+
587
+ if (typeof window !== "undefined") {
588
+ window.AtProtoMembers = window.AtProtoMembers || {};
589
+ window.AtProtoMembers.init = function (force) {
590
+ init(!!force);
591
+ };
592
+ window.AtProtoMembers.refresh = function () {
593
+ init(true);
594
+ };
595
+ }
596
+ })();