webmention-feed 0.1.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.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # webmention-feed
2
+
3
+ A Lit web component that fetches and displays [webmentions](https://indieweb.org/Webmention) for a given page URL.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install webmention-feed
9
+ ```
10
+
11
+ Or drop `dist/webmention-feed.js` directly into your project.
12
+
13
+ ## Usage
14
+
15
+ ```html
16
+ <script type="module" src="/webmention-feed.js"></script>
17
+
18
+ <webmention-feed
19
+ post-url="https://example.com/blog/my-post"
20
+ endpoint="https://webmention.io/example.com/webmention"
21
+ ></webmention-feed>
22
+ ```
23
+
24
+ ## Attributes
25
+
26
+ | Attribute | Description |
27
+ |---|---|
28
+ | `post-url` | Canonical URL of the page to fetch mentions for |
29
+ | `endpoint` | POST endpoint for submitting new webmentions |
30
+ | `fetch-endpoint` | GET endpoint for fetching mentions (default: webmention.io) |
31
+
32
+ ## CSS Custom Properties
33
+
34
+ | Property | Default | Description |
35
+ |---|---|---|
36
+ | `--wm-accent-color` | `#2563eb` | Links and button background |
37
+ | `--wm-text-color` | `inherit` | Body text |
38
+ | `--wm-border-color` | `#6b7280` | Top separator border |
39
+ | `--wm-reply-bg` | `transparent` | Reply card background |
40
+ | `--wm-reply-border-color` | `#d1d5db` | Reply card border |
41
+ | `--wm-input-bg` | `#ffffff` | URL input background |
42
+ | `--wm-input-border-color` | `#9ca3af` | URL input border |
43
+ | `--wm-avatar-bg` | `#9ca3af` | Placeholder avatar background |
44
+ | `--wm-button-text-color` | `#ffffff` | Send button text |
45
+
46
+ ## CSS Parts
47
+
48
+ Target internals with `webmention-feed::part(name)`.
49
+
50
+ `base` · `heading` · `send-form` · `input` · `button` · `list` · `reactions` · `stat` · `replies` · `reply` · `reply-meta` · `avatar` · `reply-author` · `reply-date` · `reply-link` · `reply-content` · `status`
51
+
52
+ ## Slots
53
+
54
+ | Slot | Default | Description |
55
+ |---|---|---|
56
+ | `like-icon` | ♥ | Icon before the like count |
57
+ | `repost-icon` | ↩ | Icon before the repost count |
@@ -0,0 +1,62 @@
1
+ import { LitElement } from "lit";
2
+ /**
3
+ * `<webmention-feed>` — fetches and displays webmentions for a given page.
4
+ *
5
+ * @attr {string} post-url - Canonical URL of the page to fetch mentions for.
6
+ * @attr {string} endpoint - POST endpoint for submitting new webmentions.
7
+ * @attr {string} fetch-endpoint - GET endpoint for fetching mentions (JF2 format). Defaults to webmention.io.
8
+ *
9
+ * @slot like-icon - Icon before the like count. Default: ♥
10
+ * @slot repost-icon - Icon before the repost count. Default: ↩
11
+ *
12
+ * @cssvar --wm-accent-color
13
+ * @cssvar --wm-text-color
14
+ * @cssvar --wm-border-color
15
+ * @cssvar --wm-reply-bg
16
+ * @cssvar --wm-reply-border-color
17
+ * @cssvar --wm-input-bg
18
+ * @cssvar --wm-input-border-color
19
+ * @cssvar --wm-avatar-bg
20
+ * @cssvar --wm-button-text-color
21
+ *
22
+ * @csspart base
23
+ * @csspart heading
24
+ * @csspart send-form
25
+ * @csspart input
26
+ * @csspart button
27
+ * @csspart list
28
+ * @csspart reactions
29
+ * @csspart stat
30
+ * @csspart replies
31
+ * @csspart reply
32
+ * @csspart reply-meta
33
+ * @csspart avatar
34
+ * @csspart reply-author
35
+ * @csspart reply-date
36
+ * @csspart reply-link
37
+ * @csspart reply-content
38
+ * @csspart status
39
+ */
40
+ export declare class WebmentionFeed extends LitElement {
41
+ postUrl: string;
42
+ endpoint: string;
43
+ fetchEndpoint: string;
44
+ private mentions;
45
+ private loading;
46
+ private error;
47
+ connectedCallback(): void;
48
+ updated(changed: Map<string, unknown>): void;
49
+ private fetchMentions;
50
+ private get likes();
51
+ private get reposts();
52
+ private get replies();
53
+ private renderReply;
54
+ render(): import("lit").TemplateResult<1>;
55
+ static styles: import("lit").CSSResult;
56
+ }
57
+ declare global {
58
+ interface HTMLElementTagNameMap {
59
+ "webmention-feed": WebmentionFeed;
60
+ }
61
+ }
62
+ //# sourceMappingURL=webmention-feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webmention-feed.d.ts","sourceRoot":"","sources":["../src/webmention-feed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAiBrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBACa,cAAe,SAAQ,UAAU;IACO,OAAO,SAAM;IACb,QAAQ,SAAM;IAEjE,aAAa,SAA4C;IAEhD,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,KAAK,CAAS;IAE/B,iBAAiB;IAKjB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;YAMvB,aAAa;IAsC3B,OAAO,KAAK,KAAK,GAEhB;IACD,OAAO,KAAK,OAAO,GAElB;IACD,OAAO,KAAK,OAAO,GAIlB;IAED,OAAO,CAAC,WAAW;IA+BnB,MAAM;IAyDN,MAAM,CAAC,MAAM,0BAgHX;CACH;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,cAAc,CAAC;KACnC;CACF"}
@@ -0,0 +1,849 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2019 Google LLC
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ */
6
+ const N = globalThis, V = N.ShadowRoot && (N.ShadyCSS === void 0 || N.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, Z = Symbol(), G = /* @__PURE__ */ new WeakMap();
7
+ let lt = class {
8
+ constructor(t, e, s) {
9
+ if (this._$cssResult$ = !0, s !== Z) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
10
+ this.cssText = t, this.t = e;
11
+ }
12
+ get styleSheet() {
13
+ let t = this.o;
14
+ const e = this.t;
15
+ if (V && t === void 0) {
16
+ const s = e !== void 0 && e.length === 1;
17
+ s && (t = G.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), s && G.set(e, t));
18
+ }
19
+ return t;
20
+ }
21
+ toString() {
22
+ return this.cssText;
23
+ }
24
+ };
25
+ const ut = (i) => new lt(typeof i == "string" ? i : i + "", void 0, Z), $t = (i, ...t) => {
26
+ const e = i.length === 1 ? i[0] : t.reduce((s, r, o) => s + ((n) => {
27
+ if (n._$cssResult$ === !0) return n.cssText;
28
+ if (typeof n == "number") return n;
29
+ throw Error("Value passed to 'css' function must be a 'css' function result: " + n + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
30
+ })(r) + i[o + 1], i[0]);
31
+ return new lt(e, i, Z);
32
+ }, ft = (i, t) => {
33
+ if (V) i.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet);
34
+ else for (const e of t) {
35
+ const s = document.createElement("style"), r = N.litNonce;
36
+ r !== void 0 && s.setAttribute("nonce", r), s.textContent = e.cssText, i.appendChild(s);
37
+ }
38
+ }, Q = V ? (i) => i : (i) => i instanceof CSSStyleSheet ? ((t) => {
39
+ let e = "";
40
+ for (const s of t.cssRules) e += s.cssText;
41
+ return ut(e);
42
+ })(i) : i;
43
+ /**
44
+ * @license
45
+ * Copyright 2017 Google LLC
46
+ * SPDX-License-Identifier: BSD-3-Clause
47
+ */
48
+ const { is: mt, defineProperty: yt, getOwnPropertyDescriptor: gt, getOwnPropertyNames: _t, getOwnPropertySymbols: vt, getPrototypeOf: At } = Object, g = globalThis, X = g.trustedTypes, bt = X ? X.emptyScript : "", I = g.reactiveElementPolyfillSupport, P = (i, t) => i, z = { toAttribute(i, t) {
49
+ switch (t) {
50
+ case Boolean:
51
+ i = i ? bt : null;
52
+ break;
53
+ case Object:
54
+ case Array:
55
+ i = i == null ? i : JSON.stringify(i);
56
+ }
57
+ return i;
58
+ }, fromAttribute(i, t) {
59
+ let e = i;
60
+ switch (t) {
61
+ case Boolean:
62
+ e = i !== null;
63
+ break;
64
+ case Number:
65
+ e = i === null ? null : Number(i);
66
+ break;
67
+ case Object:
68
+ case Array:
69
+ try {
70
+ e = JSON.parse(i);
71
+ } catch {
72
+ e = null;
73
+ }
74
+ }
75
+ return e;
76
+ } }, F = (i, t) => !mt(i, t), Y = { attribute: !0, type: String, converter: z, reflect: !1, useDefault: !1, hasChanged: F };
77
+ Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), g.litPropertyMetadata ?? (g.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
78
+ let E = class extends HTMLElement {
79
+ static addInitializer(t) {
80
+ this._$Ei(), (this.l ?? (this.l = [])).push(t);
81
+ }
82
+ static get observedAttributes() {
83
+ return this.finalize(), this._$Eh && [...this._$Eh.keys()];
84
+ }
85
+ static createProperty(t, e = Y) {
86
+ if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) {
87
+ const s = Symbol(), r = this.getPropertyDescriptor(t, s, e);
88
+ r !== void 0 && yt(this.prototype, t, r);
89
+ }
90
+ }
91
+ static getPropertyDescriptor(t, e, s) {
92
+ const { get: r, set: o } = gt(this.prototype, t) ?? { get() {
93
+ return this[e];
94
+ }, set(n) {
95
+ this[e] = n;
96
+ } };
97
+ return { get: r, set(n) {
98
+ const l = r == null ? void 0 : r.call(this);
99
+ o == null || o.call(this, n), this.requestUpdate(t, l, s);
100
+ }, configurable: !0, enumerable: !0 };
101
+ }
102
+ static getPropertyOptions(t) {
103
+ return this.elementProperties.get(t) ?? Y;
104
+ }
105
+ static _$Ei() {
106
+ if (this.hasOwnProperty(P("elementProperties"))) return;
107
+ const t = At(this);
108
+ t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties);
109
+ }
110
+ static finalize() {
111
+ if (this.hasOwnProperty(P("finalized"))) return;
112
+ if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(P("properties"))) {
113
+ const e = this.properties, s = [..._t(e), ...vt(e)];
114
+ for (const r of s) this.createProperty(r, e[r]);
115
+ }
116
+ const t = this[Symbol.metadata];
117
+ if (t !== null) {
118
+ const e = litPropertyMetadata.get(t);
119
+ if (e !== void 0) for (const [s, r] of e) this.elementProperties.set(s, r);
120
+ }
121
+ this._$Eh = /* @__PURE__ */ new Map();
122
+ for (const [e, s] of this.elementProperties) {
123
+ const r = this._$Eu(e, s);
124
+ r !== void 0 && this._$Eh.set(r, e);
125
+ }
126
+ this.elementStyles = this.finalizeStyles(this.styles);
127
+ }
128
+ static finalizeStyles(t) {
129
+ const e = [];
130
+ if (Array.isArray(t)) {
131
+ const s = new Set(t.flat(1 / 0).reverse());
132
+ for (const r of s) e.unshift(Q(r));
133
+ } else t !== void 0 && e.push(Q(t));
134
+ return e;
135
+ }
136
+ static _$Eu(t, e) {
137
+ const s = e.attribute;
138
+ return s === !1 ? void 0 : typeof s == "string" ? s : typeof t == "string" ? t.toLowerCase() : void 0;
139
+ }
140
+ constructor() {
141
+ super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev();
142
+ }
143
+ _$Ev() {
144
+ var t;
145
+ this._$ES = new Promise((e) => this.enableUpdating = e), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (t = this.constructor.l) == null || t.forEach((e) => e(this));
146
+ }
147
+ addController(t) {
148
+ var e;
149
+ (this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t), this.renderRoot !== void 0 && this.isConnected && ((e = t.hostConnected) == null || e.call(t));
150
+ }
151
+ removeController(t) {
152
+ var e;
153
+ (e = this._$EO) == null || e.delete(t);
154
+ }
155
+ _$E_() {
156
+ const t = /* @__PURE__ */ new Map(), e = this.constructor.elementProperties;
157
+ for (const s of e.keys()) this.hasOwnProperty(s) && (t.set(s, this[s]), delete this[s]);
158
+ t.size > 0 && (this._$Ep = t);
159
+ }
160
+ createRenderRoot() {
161
+ const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
162
+ return ft(t, this.constructor.elementStyles), t;
163
+ }
164
+ connectedCallback() {
165
+ var t;
166
+ this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(!0), (t = this._$EO) == null || t.forEach((e) => {
167
+ var s;
168
+ return (s = e.hostConnected) == null ? void 0 : s.call(e);
169
+ });
170
+ }
171
+ enableUpdating(t) {
172
+ }
173
+ disconnectedCallback() {
174
+ var t;
175
+ (t = this._$EO) == null || t.forEach((e) => {
176
+ var s;
177
+ return (s = e.hostDisconnected) == null ? void 0 : s.call(e);
178
+ });
179
+ }
180
+ attributeChangedCallback(t, e, s) {
181
+ this._$AK(t, s);
182
+ }
183
+ _$ET(t, e) {
184
+ var o;
185
+ const s = this.constructor.elementProperties.get(t), r = this.constructor._$Eu(t, s);
186
+ if (r !== void 0 && s.reflect === !0) {
187
+ const n = (((o = s.converter) == null ? void 0 : o.toAttribute) !== void 0 ? s.converter : z).toAttribute(e, s.type);
188
+ this._$Em = t, n == null ? this.removeAttribute(r) : this.setAttribute(r, n), this._$Em = null;
189
+ }
190
+ }
191
+ _$AK(t, e) {
192
+ var o, n;
193
+ const s = this.constructor, r = s._$Eh.get(t);
194
+ if (r !== void 0 && this._$Em !== r) {
195
+ const l = s.getPropertyOptions(r), a = typeof l.converter == "function" ? { fromAttribute: l.converter } : ((o = l.converter) == null ? void 0 : o.fromAttribute) !== void 0 ? l.converter : z;
196
+ this._$Em = r;
197
+ const d = a.fromAttribute(e, l.type);
198
+ this[r] = d ?? ((n = this._$Ej) == null ? void 0 : n.get(r)) ?? d, this._$Em = null;
199
+ }
200
+ }
201
+ requestUpdate(t, e, s, r = !1, o) {
202
+ var n;
203
+ if (t !== void 0) {
204
+ const l = this.constructor;
205
+ if (r === !1 && (o = this[t]), s ?? (s = l.getPropertyOptions(t)), !((s.hasChanged ?? F)(o, e) || s.useDefault && s.reflect && o === ((n = this._$Ej) == null ? void 0 : n.get(t)) && !this.hasAttribute(l._$Eu(t, s)))) return;
206
+ this.C(t, e, s);
207
+ }
208
+ this.isUpdatePending === !1 && (this._$ES = this._$EP());
209
+ }
210
+ C(t, e, { useDefault: s, reflect: r, wrapped: o }, n) {
211
+ s && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t) && (this._$Ej.set(t, n ?? e ?? this[t]), o !== !0 || n !== void 0) || (this._$AL.has(t) || (this.hasUpdated || s || (e = void 0), this._$AL.set(t, e)), r === !0 && this._$Em !== t && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t));
212
+ }
213
+ async _$EP() {
214
+ this.isUpdatePending = !0;
215
+ try {
216
+ await this._$ES;
217
+ } catch (e) {
218
+ Promise.reject(e);
219
+ }
220
+ const t = this.scheduleUpdate();
221
+ return t != null && await t, !this.isUpdatePending;
222
+ }
223
+ scheduleUpdate() {
224
+ return this.performUpdate();
225
+ }
226
+ performUpdate() {
227
+ var s;
228
+ if (!this.isUpdatePending) return;
229
+ if (!this.hasUpdated) {
230
+ if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
231
+ for (const [o, n] of this._$Ep) this[o] = n;
232
+ this._$Ep = void 0;
233
+ }
234
+ const r = this.constructor.elementProperties;
235
+ if (r.size > 0) for (const [o, n] of r) {
236
+ const { wrapped: l } = n, a = this[o];
237
+ l !== !0 || this._$AL.has(o) || a === void 0 || this.C(o, void 0, n, a);
238
+ }
239
+ }
240
+ let t = !1;
241
+ const e = this._$AL;
242
+ try {
243
+ t = this.shouldUpdate(e), t ? (this.willUpdate(e), (s = this._$EO) == null || s.forEach((r) => {
244
+ var o;
245
+ return (o = r.hostUpdate) == null ? void 0 : o.call(r);
246
+ }), this.update(e)) : this._$EM();
247
+ } catch (r) {
248
+ throw t = !1, this._$EM(), r;
249
+ }
250
+ t && this._$AE(e);
251
+ }
252
+ willUpdate(t) {
253
+ }
254
+ _$AE(t) {
255
+ var e;
256
+ (e = this._$EO) == null || e.forEach((s) => {
257
+ var r;
258
+ return (r = s.hostUpdated) == null ? void 0 : r.call(s);
259
+ }), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t);
260
+ }
261
+ _$EM() {
262
+ this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1;
263
+ }
264
+ get updateComplete() {
265
+ return this.getUpdateComplete();
266
+ }
267
+ getUpdateComplete() {
268
+ return this._$ES;
269
+ }
270
+ shouldUpdate(t) {
271
+ return !0;
272
+ }
273
+ update(t) {
274
+ this._$Eq && (this._$Eq = this._$Eq.forEach((e) => this._$ET(e, this[e]))), this._$EM();
275
+ }
276
+ updated(t) {
277
+ }
278
+ firstUpdated(t) {
279
+ }
280
+ };
281
+ E.elementStyles = [], E.shadowRootOptions = { mode: "open" }, E[P("elementProperties")] = /* @__PURE__ */ new Map(), E[P("finalized")] = /* @__PURE__ */ new Map(), I == null || I({ ReactiveElement: E }), (g.reactiveElementVersions ?? (g.reactiveElementVersions = [])).push("2.1.2");
282
+ /**
283
+ * @license
284
+ * Copyright 2017 Google LLC
285
+ * SPDX-License-Identifier: BSD-3-Clause
286
+ */
287
+ const U = globalThis, tt = (i) => i, j = U.trustedTypes, et = j ? j.createPolicy("lit-html", { createHTML: (i) => i }) : void 0, ht = "$lit$", y = `lit$${Math.random().toFixed(9).slice(2)}$`, ct = "?" + y, wt = `<${ct}>`, b = document, M = () => b.createComment(""), H = (i) => i === null || typeof i != "object" && typeof i != "function", J = Array.isArray, Et = (i) => J(i) || typeof (i == null ? void 0 : i[Symbol.iterator]) == "function", W = `[
288
+ \f\r]`, C = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, st = /-->/g, rt = />/g, _ = RegExp(`>|${W}(?:([^\\s"'>=/]+)(${W}*=${W}*(?:[^
289
+ \f\r"'\`<>=]|("|')|))|$)`, "g"), it = /'/g, nt = /"/g, pt = /^(?:script|style|textarea|title)$/i, St = (i) => (t, ...e) => ({ _$litType$: i, strings: t, values: e }), u = St(1), S = Symbol.for("lit-noChange"), p = Symbol.for("lit-nothing"), ot = /* @__PURE__ */ new WeakMap(), v = b.createTreeWalker(b, 129);
290
+ function dt(i, t) {
291
+ if (!J(i) || !i.hasOwnProperty("raw")) throw Error("invalid template strings array");
292
+ return et !== void 0 ? et.createHTML(t) : t;
293
+ }
294
+ const xt = (i, t) => {
295
+ const e = i.length - 1, s = [];
296
+ let r, o = t === 2 ? "<svg>" : t === 3 ? "<math>" : "", n = C;
297
+ for (let l = 0; l < e; l++) {
298
+ const a = i[l];
299
+ let d, c, h = -1, $ = 0;
300
+ for (; $ < a.length && (n.lastIndex = $, c = n.exec(a), c !== null); ) $ = n.lastIndex, n === C ? c[1] === "!--" ? n = st : c[1] !== void 0 ? n = rt : c[2] !== void 0 ? (pt.test(c[2]) && (r = RegExp("</" + c[2], "g")), n = _) : c[3] !== void 0 && (n = _) : n === _ ? c[0] === ">" ? (n = r ?? C, h = -1) : c[1] === void 0 ? h = -2 : (h = n.lastIndex - c[2].length, d = c[1], n = c[3] === void 0 ? _ : c[3] === '"' ? nt : it) : n === nt || n === it ? n = _ : n === st || n === rt ? n = C : (n = _, r = void 0);
301
+ const m = n === _ && i[l + 1].startsWith("/>") ? " " : "";
302
+ o += n === C ? a + wt : h >= 0 ? (s.push(d), a.slice(0, h) + ht + a.slice(h) + y + m) : a + y + (h === -2 ? l : m);
303
+ }
304
+ return [dt(i, o + (i[e] || "<?>") + (t === 2 ? "</svg>" : t === 3 ? "</math>" : "")), s];
305
+ };
306
+ class k {
307
+ constructor({ strings: t, _$litType$: e }, s) {
308
+ let r;
309
+ this.parts = [];
310
+ let o = 0, n = 0;
311
+ const l = t.length - 1, a = this.parts, [d, c] = xt(t, e);
312
+ if (this.el = k.createElement(d, s), v.currentNode = this.el.content, e === 2 || e === 3) {
313
+ const h = this.el.content.firstChild;
314
+ h.replaceWith(...h.childNodes);
315
+ }
316
+ for (; (r = v.nextNode()) !== null && a.length < l; ) {
317
+ if (r.nodeType === 1) {
318
+ if (r.hasAttributes()) for (const h of r.getAttributeNames()) if (h.endsWith(ht)) {
319
+ const $ = c[n++], m = r.getAttribute(h).split(y), R = /([.?@])?(.*)/.exec($);
320
+ a.push({ type: 1, index: o, name: R[2], strings: m, ctor: R[1] === "." ? Pt : R[1] === "?" ? Ut : R[1] === "@" ? Ot : D }), r.removeAttribute(h);
321
+ } else h.startsWith(y) && (a.push({ type: 6, index: o }), r.removeAttribute(h));
322
+ if (pt.test(r.tagName)) {
323
+ const h = r.textContent.split(y), $ = h.length - 1;
324
+ if ($ > 0) {
325
+ r.textContent = j ? j.emptyScript : "";
326
+ for (let m = 0; m < $; m++) r.append(h[m], M()), v.nextNode(), a.push({ type: 2, index: ++o });
327
+ r.append(h[$], M());
328
+ }
329
+ }
330
+ } else if (r.nodeType === 8) if (r.data === ct) a.push({ type: 2, index: o });
331
+ else {
332
+ let h = -1;
333
+ for (; (h = r.data.indexOf(y, h + 1)) !== -1; ) a.push({ type: 7, index: o }), h += y.length - 1;
334
+ }
335
+ o++;
336
+ }
337
+ }
338
+ static createElement(t, e) {
339
+ const s = b.createElement("template");
340
+ return s.innerHTML = t, s;
341
+ }
342
+ }
343
+ function x(i, t, e = i, s) {
344
+ var n, l;
345
+ if (t === S) return t;
346
+ let r = s !== void 0 ? (n = e._$Co) == null ? void 0 : n[s] : e._$Cl;
347
+ const o = H(t) ? void 0 : t._$litDirective$;
348
+ return (r == null ? void 0 : r.constructor) !== o && ((l = r == null ? void 0 : r._$AO) == null || l.call(r, !1), o === void 0 ? r = void 0 : (r = new o(i), r._$AT(i, e, s)), s !== void 0 ? (e._$Co ?? (e._$Co = []))[s] = r : e._$Cl = r), r !== void 0 && (t = x(i, r._$AS(i, t.values), r, s)), t;
349
+ }
350
+ class Ct {
351
+ constructor(t, e) {
352
+ this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = e;
353
+ }
354
+ get parentNode() {
355
+ return this._$AM.parentNode;
356
+ }
357
+ get _$AU() {
358
+ return this._$AM._$AU;
359
+ }
360
+ u(t) {
361
+ const { el: { content: e }, parts: s } = this._$AD, r = ((t == null ? void 0 : t.creationScope) ?? b).importNode(e, !0);
362
+ v.currentNode = r;
363
+ let o = v.nextNode(), n = 0, l = 0, a = s[0];
364
+ for (; a !== void 0; ) {
365
+ if (n === a.index) {
366
+ let d;
367
+ a.type === 2 ? d = new T(o, o.nextSibling, this, t) : a.type === 1 ? d = new a.ctor(o, a.name, a.strings, this, t) : a.type === 6 && (d = new Mt(o, this, t)), this._$AV.push(d), a = s[++l];
368
+ }
369
+ n !== (a == null ? void 0 : a.index) && (o = v.nextNode(), n++);
370
+ }
371
+ return v.currentNode = b, r;
372
+ }
373
+ p(t) {
374
+ let e = 0;
375
+ for (const s of this._$AV) s !== void 0 && (s.strings !== void 0 ? (s._$AI(t, s, e), e += s.strings.length - 2) : s._$AI(t[e])), e++;
376
+ }
377
+ }
378
+ class T {
379
+ get _$AU() {
380
+ var t;
381
+ return ((t = this._$AM) == null ? void 0 : t._$AU) ?? this._$Cv;
382
+ }
383
+ constructor(t, e, s, r) {
384
+ this.type = 2, this._$AH = p, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = s, this.options = r, this._$Cv = (r == null ? void 0 : r.isConnected) ?? !0;
385
+ }
386
+ get parentNode() {
387
+ let t = this._$AA.parentNode;
388
+ const e = this._$AM;
389
+ return e !== void 0 && (t == null ? void 0 : t.nodeType) === 11 && (t = e.parentNode), t;
390
+ }
391
+ get startNode() {
392
+ return this._$AA;
393
+ }
394
+ get endNode() {
395
+ return this._$AB;
396
+ }
397
+ _$AI(t, e = this) {
398
+ t = x(this, t, e), H(t) ? t === p || t == null || t === "" ? (this._$AH !== p && this._$AR(), this._$AH = p) : t !== this._$AH && t !== S && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : Et(t) ? this.k(t) : this._(t);
399
+ }
400
+ O(t) {
401
+ return this._$AA.parentNode.insertBefore(t, this._$AB);
402
+ }
403
+ T(t) {
404
+ this._$AH !== t && (this._$AR(), this._$AH = this.O(t));
405
+ }
406
+ _(t) {
407
+ this._$AH !== p && H(this._$AH) ? this._$AA.nextSibling.data = t : this.T(b.createTextNode(t)), this._$AH = t;
408
+ }
409
+ $(t) {
410
+ var o;
411
+ const { values: e, _$litType$: s } = t, r = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = k.createElement(dt(s.h, s.h[0]), this.options)), s);
412
+ if (((o = this._$AH) == null ? void 0 : o._$AD) === r) this._$AH.p(e);
413
+ else {
414
+ const n = new Ct(r, this), l = n.u(this.options);
415
+ n.p(e), this.T(l), this._$AH = n;
416
+ }
417
+ }
418
+ _$AC(t) {
419
+ let e = ot.get(t.strings);
420
+ return e === void 0 && ot.set(t.strings, e = new k(t)), e;
421
+ }
422
+ k(t) {
423
+ J(this._$AH) || (this._$AH = [], this._$AR());
424
+ const e = this._$AH;
425
+ let s, r = 0;
426
+ for (const o of t) r === e.length ? e.push(s = new T(this.O(M()), this.O(M()), this, this.options)) : s = e[r], s._$AI(o), r++;
427
+ r < e.length && (this._$AR(s && s._$AB.nextSibling, r), e.length = r);
428
+ }
429
+ _$AR(t = this._$AA.nextSibling, e) {
430
+ var s;
431
+ for ((s = this._$AP) == null ? void 0 : s.call(this, !1, !0, e); t !== this._$AB; ) {
432
+ const r = tt(t).nextSibling;
433
+ tt(t).remove(), t = r;
434
+ }
435
+ }
436
+ setConnected(t) {
437
+ var e;
438
+ this._$AM === void 0 && (this._$Cv = t, (e = this._$AP) == null || e.call(this, t));
439
+ }
440
+ }
441
+ class D {
442
+ get tagName() {
443
+ return this.element.tagName;
444
+ }
445
+ get _$AU() {
446
+ return this._$AM._$AU;
447
+ }
448
+ constructor(t, e, s, r, o) {
449
+ this.type = 1, this._$AH = p, this._$AN = void 0, this.element = t, this.name = e, this._$AM = r, this.options = o, s.length > 2 || s[0] !== "" || s[1] !== "" ? (this._$AH = Array(s.length - 1).fill(new String()), this.strings = s) : this._$AH = p;
450
+ }
451
+ _$AI(t, e = this, s, r) {
452
+ const o = this.strings;
453
+ let n = !1;
454
+ if (o === void 0) t = x(this, t, e, 0), n = !H(t) || t !== this._$AH && t !== S, n && (this._$AH = t);
455
+ else {
456
+ const l = t;
457
+ let a, d;
458
+ for (t = o[0], a = 0; a < o.length - 1; a++) d = x(this, l[s + a], e, a), d === S && (d = this._$AH[a]), n || (n = !H(d) || d !== this._$AH[a]), d === p ? t = p : t !== p && (t += (d ?? "") + o[a + 1]), this._$AH[a] = d;
459
+ }
460
+ n && !r && this.j(t);
461
+ }
462
+ j(t) {
463
+ t === p ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? "");
464
+ }
465
+ }
466
+ class Pt extends D {
467
+ constructor() {
468
+ super(...arguments), this.type = 3;
469
+ }
470
+ j(t) {
471
+ this.element[this.name] = t === p ? void 0 : t;
472
+ }
473
+ }
474
+ class Ut extends D {
475
+ constructor() {
476
+ super(...arguments), this.type = 4;
477
+ }
478
+ j(t) {
479
+ this.element.toggleAttribute(this.name, !!t && t !== p);
480
+ }
481
+ }
482
+ class Ot extends D {
483
+ constructor(t, e, s, r, o) {
484
+ super(t, e, s, r, o), this.type = 5;
485
+ }
486
+ _$AI(t, e = this) {
487
+ if ((t = x(this, t, e, 0) ?? p) === S) return;
488
+ const s = this._$AH, r = t === p && s !== p || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, o = t !== p && (s === p || r);
489
+ r && this.element.removeEventListener(this.name, this, s), o && this.element.addEventListener(this.name, this, t), this._$AH = t;
490
+ }
491
+ handleEvent(t) {
492
+ var e;
493
+ typeof this._$AH == "function" ? this._$AH.call(((e = this.options) == null ? void 0 : e.host) ?? this.element, t) : this._$AH.handleEvent(t);
494
+ }
495
+ }
496
+ class Mt {
497
+ constructor(t, e, s) {
498
+ this.element = t, this.type = 6, this._$AN = void 0, this._$AM = e, this.options = s;
499
+ }
500
+ get _$AU() {
501
+ return this._$AM._$AU;
502
+ }
503
+ _$AI(t) {
504
+ x(this, t);
505
+ }
506
+ }
507
+ const B = U.litHtmlPolyfillSupport;
508
+ B == null || B(k, T), (U.litHtmlVersions ?? (U.litHtmlVersions = [])).push("3.3.2");
509
+ const Ht = (i, t, e) => {
510
+ const s = (e == null ? void 0 : e.renderBefore) ?? t;
511
+ let r = s._$litPart$;
512
+ if (r === void 0) {
513
+ const o = (e == null ? void 0 : e.renderBefore) ?? null;
514
+ s._$litPart$ = r = new T(t.insertBefore(M(), o), o, void 0, e ?? {});
515
+ }
516
+ return r._$AI(i), r;
517
+ };
518
+ /**
519
+ * @license
520
+ * Copyright 2017 Google LLC
521
+ * SPDX-License-Identifier: BSD-3-Clause
522
+ */
523
+ const A = globalThis;
524
+ class O extends E {
525
+ constructor() {
526
+ super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
527
+ }
528
+ createRenderRoot() {
529
+ var e;
530
+ const t = super.createRenderRoot();
531
+ return (e = this.renderOptions).renderBefore ?? (e.renderBefore = t.firstChild), t;
532
+ }
533
+ update(t) {
534
+ const e = this.render();
535
+ this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = Ht(e, this.renderRoot, this.renderOptions);
536
+ }
537
+ connectedCallback() {
538
+ var t;
539
+ super.connectedCallback(), (t = this._$Do) == null || t.setConnected(!0);
540
+ }
541
+ disconnectedCallback() {
542
+ var t;
543
+ super.disconnectedCallback(), (t = this._$Do) == null || t.setConnected(!1);
544
+ }
545
+ render() {
546
+ return S;
547
+ }
548
+ }
549
+ var at;
550
+ O._$litElement$ = !0, O.finalized = !0, (at = A.litElementHydrateSupport) == null || at.call(A, { LitElement: O });
551
+ const q = A.litElementPolyfillSupport;
552
+ q == null || q({ LitElement: O });
553
+ (A.litElementVersions ?? (A.litElementVersions = [])).push("4.2.2");
554
+ /**
555
+ * @license
556
+ * Copyright 2017 Google LLC
557
+ * SPDX-License-Identifier: BSD-3-Clause
558
+ */
559
+ const kt = (i) => (t, e) => {
560
+ e !== void 0 ? e.addInitializer(() => {
561
+ customElements.define(i, t);
562
+ }) : customElements.define(i, t);
563
+ };
564
+ /**
565
+ * @license
566
+ * Copyright 2017 Google LLC
567
+ * SPDX-License-Identifier: BSD-3-Clause
568
+ */
569
+ const Tt = { attribute: !0, type: String, converter: z, reflect: !1, hasChanged: F }, Rt = (i = Tt, t, e) => {
570
+ const { kind: s, metadata: r } = e;
571
+ let o = globalThis.litPropertyMetadata.get(r);
572
+ if (o === void 0 && globalThis.litPropertyMetadata.set(r, o = /* @__PURE__ */ new Map()), s === "setter" && ((i = Object.create(i)).wrapped = !0), o.set(e.name, i), s === "accessor") {
573
+ const { name: n } = e;
574
+ return { set(l) {
575
+ const a = t.get.call(this);
576
+ t.set.call(this, l), this.requestUpdate(n, a, i, !0, l);
577
+ }, init(l) {
578
+ return l !== void 0 && this.C(n, void 0, i, l), l;
579
+ } };
580
+ }
581
+ if (s === "setter") {
582
+ const { name: n } = e;
583
+ return function(l) {
584
+ const a = this[n];
585
+ t.call(this, l), this.requestUpdate(n, a, i, !0, l);
586
+ };
587
+ }
588
+ throw Error("Unsupported decorator location: " + s);
589
+ };
590
+ function L(i) {
591
+ return (t, e) => typeof e == "object" ? Rt(i, t, e) : ((s, r, o) => {
592
+ const n = r.hasOwnProperty(o);
593
+ return r.constructor.createProperty(o, s), n ? Object.getOwnPropertyDescriptor(r, o) : void 0;
594
+ })(i, t, e);
595
+ }
596
+ /**
597
+ * @license
598
+ * Copyright 2017 Google LLC
599
+ * SPDX-License-Identifier: BSD-3-Clause
600
+ */
601
+ function K(i) {
602
+ return L({ ...i, state: !0, attribute: !1 });
603
+ }
604
+ var Nt = Object.defineProperty, zt = Object.getOwnPropertyDescriptor, w = (i, t, e, s) => {
605
+ for (var r = s > 1 ? void 0 : s ? zt(t, e) : t, o = i.length - 1, n; o >= 0; o--)
606
+ (n = i[o]) && (r = (s ? n(t, e, r) : n(r)) || r);
607
+ return s && r && Nt(t, e, r), r;
608
+ };
609
+ let f = class extends O {
610
+ constructor() {
611
+ super(...arguments), this.postUrl = "", this.endpoint = "", this.fetchEndpoint = "https://webmention.io/api/mentions.jf2", this.mentions = [], this.loading = !0, this.error = !1;
612
+ }
613
+ connectedCallback() {
614
+ super.connectedCallback(), this.postUrl && this.fetchMentions();
615
+ }
616
+ updated(i) {
617
+ i.has("postUrl") && this.postUrl && this.fetchMentions();
618
+ }
619
+ async fetchMentions() {
620
+ this.loading = !0, this.error = !1;
621
+ try {
622
+ const i = (c) => c.endsWith("/") ? c : `${c}/`, t = i(this.postUrl), e = new URL(t), r = e.hostname.startsWith("www.") ? e.hostname.slice(4) : `www.${e.hostname}`, o = i(
623
+ `${e.protocol}//${r}${e.pathname}${e.search}`
624
+ ), n = async (c) => {
625
+ const h = await fetch(
626
+ `${this.fetchEndpoint}?target=${encodeURIComponent(c)}&per-page=100`
627
+ );
628
+ if (!h.ok) throw new Error(`${h.status}`);
629
+ return (await h.json()).children ?? [];
630
+ }, [l, a] = await Promise.all([n(t), n(o)]), d = /* @__PURE__ */ new Set();
631
+ this.mentions = [...l, ...a].filter((c) => d.has(c.url) ? !1 : (d.add(c.url), !0));
632
+ } catch {
633
+ this.error = !0;
634
+ } finally {
635
+ this.loading = !1;
636
+ }
637
+ }
638
+ get likes() {
639
+ return this.mentions.filter((i) => i["wm-property"] === "like-of");
640
+ }
641
+ get reposts() {
642
+ return this.mentions.filter((i) => i["wm-property"] === "repost-of");
643
+ }
644
+ get replies() {
645
+ return this.mentions.filter(
646
+ (i) => i["wm-property"] === "in-reply-to" || i["wm-property"] === "mention-of"
647
+ );
648
+ }
649
+ renderReply(i) {
650
+ var s;
651
+ const t = i.author ?? {}, e = i.published ? new Date(i.published).toLocaleDateString("en-US", {
652
+ year: "numeric",
653
+ month: "short",
654
+ day: "numeric"
655
+ }) : null;
656
+ return u`
657
+ <li class="reply" part="reply">
658
+ <div class="reply-meta" part="reply-meta">
659
+ ${t.photo ? u`<img src=${t.photo} alt=${t.name ?? ""} class="avatar" part="avatar" width="28" height="28" />` : u`<span class="avatar placeholder" part="avatar"></span>`}
660
+ <span class="reply-author" part="reply-author">
661
+ ${t.url ? u`<a href=${t.url} rel="noopener noreferrer">${t.name ?? t.url}</a>` : t.name ?? "Anonymous"}
662
+ </span>
663
+ ${e ? u`<time class="reply-date" part="reply-date">${e}</time>` : p}
664
+ <a href=${i.url} class="reply-link" part="reply-link" rel="noopener noreferrer">→</a>
665
+ </div>
666
+ ${(s = i.content) != null && s.text ? u`<p class="reply-content" part="reply-content">${i.content.text}</p>` : p}
667
+ </li>
668
+ `;
669
+ }
670
+ render() {
671
+ return u`
672
+ <section part="base">
673
+ <h2 part="heading">Webmentions</h2>
674
+
675
+ <div class="send" part="send-form">
676
+ <p>Written a response? Send a webmention:</p>
677
+ <form action=${this.endpoint} method="post">
678
+ <input type="hidden" name="target" .value=${this.postUrl} />
679
+ <input
680
+ type="url"
681
+ name="source"
682
+ placeholder="https://your-post-url.com"
683
+ required
684
+ part="input"
685
+ />
686
+ <button type="submit" part="button">Send</button>
687
+ </form>
688
+ </div>
689
+
690
+ <div class="list" part="list">
691
+ ${this.loading ? u`<p class="status" part="status">Loading mentions…</p>` : this.error ? p : this.mentions.length === 0 ? u`<p class="status" part="status">No mentions yet.</p>` : u`
692
+ ${this.likes.length > 0 || this.reposts.length > 0 ? u`
693
+ <div class="reactions" part="reactions">
694
+ ${this.likes.length > 0 ? u`<span class="stat" part="stat">
695
+ <slot name="like-icon">♥</slot>
696
+ ${this.likes.length} like${this.likes.length !== 1 ? "s" : ""}
697
+ </span>` : p}
698
+ ${this.reposts.length > 0 ? u`<span class="stat" part="stat">
699
+ <slot name="repost-icon">↩</slot>
700
+ ${this.reposts.length} repost${this.reposts.length !== 1 ? "s" : ""}
701
+ </span>` : p}
702
+ </div>
703
+ ` : p}
704
+ ${this.replies.length > 0 ? u`<ol class="replies" part="replies">
705
+ ${this.replies.map((i) => this.renderReply(i))}
706
+ </ol>` : p}
707
+ `}
708
+ </div>
709
+ </section>
710
+ `;
711
+ }
712
+ };
713
+ f.styles = $t`
714
+ :host {
715
+ --wm-text-color: inherit;
716
+ --wm-accent-color: #2563eb;
717
+ --wm-border-color: #6b7280;
718
+ --wm-reply-bg: transparent;
719
+ --wm-reply-border-color: #d1d5db;
720
+ --wm-input-bg: #ffffff;
721
+ --wm-input-border-color: #9ca3af;
722
+ --wm-avatar-bg: #9ca3af;
723
+ --wm-button-text-color: #ffffff;
724
+
725
+ display: block;
726
+ color: var(--wm-text-color);
727
+ margin-top: 3rem;
728
+ padding-top: 2rem;
729
+ border-top: 1px solid var(--wm-border-color);
730
+ }
731
+
732
+ h2 { margin: 0 0 1.5rem; }
733
+
734
+ .send p {
735
+ font-size: 0.9rem;
736
+ opacity: 0.75;
737
+ margin: 0 0 0.5rem;
738
+ }
739
+
740
+ form {
741
+ display: flex;
742
+ gap: 0.5rem;
743
+ flex-wrap: wrap;
744
+ margin-bottom: 2rem;
745
+ }
746
+
747
+ input[type="url"] {
748
+ flex: 1;
749
+ min-width: 0;
750
+ padding: 0.4rem 0.75rem;
751
+ border: 1px solid var(--wm-input-border-color);
752
+ border-radius: 6px;
753
+ background: var(--wm-input-bg);
754
+ color: var(--wm-text-color);
755
+ font-size: 0.9rem;
756
+ font-family: inherit;
757
+ }
758
+
759
+ button {
760
+ padding: 0.4rem 1rem;
761
+ border-radius: 6px;
762
+ border: none;
763
+ background: var(--wm-accent-color);
764
+ color: var(--wm-button-text-color);
765
+ font-size: 0.9rem;
766
+ font-family: inherit;
767
+ cursor: pointer;
768
+ }
769
+
770
+ .status { font-size: 0.9rem; opacity: 0.6; }
771
+
772
+ .reactions { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
773
+ .stat { font-size: 0.9rem; opacity: 0.8; }
774
+
775
+ .replies {
776
+ list-style: none;
777
+ padding: 0;
778
+ margin: 0;
779
+ display: flex;
780
+ flex-direction: column;
781
+ gap: 1.25rem;
782
+ }
783
+
784
+ .reply {
785
+ background: var(--wm-reply-bg);
786
+ border: 1px solid var(--wm-reply-border-color);
787
+ border-radius: 8px;
788
+ padding: 0.75rem 1rem;
789
+ }
790
+
791
+ .reply-meta {
792
+ display: flex;
793
+ align-items: center;
794
+ gap: 0.5rem;
795
+ flex-wrap: wrap;
796
+ margin-bottom: 0.4rem;
797
+ }
798
+
799
+ .avatar {
800
+ width: 28px;
801
+ height: 28px;
802
+ border-radius: 50%;
803
+ object-fit: cover;
804
+ flex-shrink: 0;
805
+ }
806
+
807
+ .avatar.placeholder {
808
+ display: inline-block;
809
+ background: var(--wm-avatar-bg);
810
+ }
811
+
812
+ .reply-author { font-size: 0.9rem; font-weight: 600; }
813
+ .reply-author a { color: var(--wm-accent-color); text-decoration: none; }
814
+
815
+ .reply-date { font-size: 0.8rem; opacity: 0.6; margin-left: auto; }
816
+
817
+ .reply-link { font-size: 0.85rem; color: var(--wm-accent-color); text-decoration: none; }
818
+
819
+ .reply-content {
820
+ margin: 0;
821
+ font-size: 0.9rem;
822
+ opacity: 0.85;
823
+ line-height: 1.5;
824
+ }
825
+ `;
826
+ w([
827
+ L({ type: String, attribute: "post-url" })
828
+ ], f.prototype, "postUrl", 2);
829
+ w([
830
+ L({ type: String, attribute: "endpoint" })
831
+ ], f.prototype, "endpoint", 2);
832
+ w([
833
+ L({ type: String, attribute: "fetch-endpoint" })
834
+ ], f.prototype, "fetchEndpoint", 2);
835
+ w([
836
+ K()
837
+ ], f.prototype, "mentions", 2);
838
+ w([
839
+ K()
840
+ ], f.prototype, "loading", 2);
841
+ w([
842
+ K()
843
+ ], f.prototype, "error", 2);
844
+ f = w([
845
+ kt("webmention-feed")
846
+ ], f);
847
+ export {
848
+ f as WebmentionFeed
849
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "webmention-feed",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A Lit web component for fetching and displaying webmentions",
6
+ "main": "./dist/webmention-feed.js",
7
+ "module": "./dist/webmention-feed.js",
8
+ "types": "./dist/webmention-feed.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/webmention-feed.js",
12
+ "types": "./dist/webmention-feed.d.ts"
13
+ }
14
+ },
15
+ "files": ["dist", "src"],
16
+ "scripts": {
17
+ "build": "vite build && tsc --emitDeclarationOnly --outDir dist",
18
+ "dev": "vite build --watch",
19
+ "clean": "rm -rf dist"
20
+ },
21
+ "keywords": ["webmention", "indieweb", "web-component", "lit"],
22
+ "devDependencies": {
23
+ "lit": "^3.0.0",
24
+ "vite": "^5.0.0"
25
+ }
26
+ }
@@ -0,0 +1,335 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+
4
+ interface Author {
5
+ name?: string;
6
+ photo?: string;
7
+ url?: string;
8
+ }
9
+
10
+ interface Mention {
11
+ "wm-property": "like-of" | "repost-of" | "in-reply-to" | "mention-of";
12
+ author?: Author;
13
+ url: string;
14
+ published?: string | null;
15
+ content?: { text?: string };
16
+ }
17
+
18
+ /**
19
+ * `<webmention-feed>` — fetches and displays webmentions for a given page.
20
+ *
21
+ * @attr {string} post-url - Canonical URL of the page to fetch mentions for.
22
+ * @attr {string} endpoint - POST endpoint for submitting new webmentions.
23
+ * @attr {string} fetch-endpoint - GET endpoint for fetching mentions (JF2 format). Defaults to webmention.io.
24
+ *
25
+ * @slot like-icon - Icon before the like count. Default: ♥
26
+ * @slot repost-icon - Icon before the repost count. Default: ↩
27
+ *
28
+ * @cssvar --wm-accent-color
29
+ * @cssvar --wm-text-color
30
+ * @cssvar --wm-border-color
31
+ * @cssvar --wm-reply-bg
32
+ * @cssvar --wm-reply-border-color
33
+ * @cssvar --wm-input-bg
34
+ * @cssvar --wm-input-border-color
35
+ * @cssvar --wm-avatar-bg
36
+ * @cssvar --wm-button-text-color
37
+ *
38
+ * @csspart base
39
+ * @csspart heading
40
+ * @csspart send-form
41
+ * @csspart input
42
+ * @csspart button
43
+ * @csspart list
44
+ * @csspart reactions
45
+ * @csspart stat
46
+ * @csspart replies
47
+ * @csspart reply
48
+ * @csspart reply-meta
49
+ * @csspart avatar
50
+ * @csspart reply-author
51
+ * @csspart reply-date
52
+ * @csspart reply-link
53
+ * @csspart reply-content
54
+ * @csspart status
55
+ */
56
+ @customElement("webmention-feed")
57
+ export class WebmentionFeed extends LitElement {
58
+ @property({ type: String, attribute: "post-url" }) postUrl = "";
59
+ @property({ type: String, attribute: "endpoint" }) endpoint = "";
60
+ @property({ type: String, attribute: "fetch-endpoint" })
61
+ fetchEndpoint = "https://webmention.io/api/mentions.jf2";
62
+
63
+ @state() private mentions: Mention[] = [];
64
+ @state() private loading = true;
65
+ @state() private error = false;
66
+
67
+ connectedCallback() {
68
+ super.connectedCallback();
69
+ if (this.postUrl) this.fetchMentions();
70
+ }
71
+
72
+ updated(changed: Map<string, unknown>) {
73
+ if (changed.has("postUrl") && this.postUrl) {
74
+ this.fetchMentions();
75
+ }
76
+ }
77
+
78
+ private async fetchMentions() {
79
+ this.loading = true;
80
+ this.error = false;
81
+
82
+ try {
83
+ const normalize = (u: string) => (u.endsWith("/") ? u : `${u}/`);
84
+ const url = normalize(this.postUrl);
85
+ const parsed = new URL(url);
86
+
87
+ const isWww = parsed.hostname.startsWith("www.");
88
+ const altHostname = isWww ? parsed.hostname.slice(4) : `www.${parsed.hostname}`;
89
+ const altUrl = normalize(
90
+ `${parsed.protocol}//${altHostname}${parsed.pathname}${parsed.search}`
91
+ );
92
+
93
+ const fetchTarget = async (target: string) => {
94
+ const res = await fetch(
95
+ `${this.fetchEndpoint}?target=${encodeURIComponent(target)}&per-page=100`
96
+ );
97
+ if (!res.ok) throw new Error(`${res.status}`);
98
+ const data = await res.json();
99
+ return (data.children ?? []) as Mention[];
100
+ };
101
+
102
+ const [primary, alt] = await Promise.all([fetchTarget(url), fetchTarget(altUrl)]);
103
+ const seen = new Set<string>();
104
+ this.mentions = [...primary, ...alt].filter((m) => {
105
+ if (seen.has(m.url)) return false;
106
+ seen.add(m.url);
107
+ return true;
108
+ });
109
+ } catch {
110
+ this.error = true;
111
+ } finally {
112
+ this.loading = false;
113
+ }
114
+ }
115
+
116
+ private get likes() {
117
+ return this.mentions.filter((m) => m["wm-property"] === "like-of");
118
+ }
119
+ private get reposts() {
120
+ return this.mentions.filter((m) => m["wm-property"] === "repost-of");
121
+ }
122
+ private get replies() {
123
+ return this.mentions.filter(
124
+ (m) => m["wm-property"] === "in-reply-to" || m["wm-property"] === "mention-of"
125
+ );
126
+ }
127
+
128
+ private renderReply(m: Mention) {
129
+ const author = m.author ?? {};
130
+ const date = m.published
131
+ ? new Date(m.published).toLocaleDateString("en-US", {
132
+ year: "numeric",
133
+ month: "short",
134
+ day: "numeric",
135
+ })
136
+ : null;
137
+
138
+ return html`
139
+ <li class="reply" part="reply">
140
+ <div class="reply-meta" part="reply-meta">
141
+ ${author.photo
142
+ ? html`<img src=${author.photo} alt=${author.name ?? ""} class="avatar" part="avatar" width="28" height="28" />`
143
+ : html`<span class="avatar placeholder" part="avatar"></span>`}
144
+ <span class="reply-author" part="reply-author">
145
+ ${author.url
146
+ ? html`<a href=${author.url} rel="noopener noreferrer">${author.name ?? author.url}</a>`
147
+ : (author.name ?? "Anonymous")}
148
+ </span>
149
+ ${date ? html`<time class="reply-date" part="reply-date">${date}</time>` : nothing}
150
+ <a href=${m.url} class="reply-link" part="reply-link" rel="noopener noreferrer">→</a>
151
+ </div>
152
+ ${m.content?.text
153
+ ? html`<p class="reply-content" part="reply-content">${m.content.text}</p>`
154
+ : nothing}
155
+ </li>
156
+ `;
157
+ }
158
+
159
+ render() {
160
+ return html`
161
+ <section part="base">
162
+ <h2 part="heading">Webmentions</h2>
163
+
164
+ <div class="send" part="send-form">
165
+ <p>Written a response? Send a webmention:</p>
166
+ <form action=${this.endpoint} method="post">
167
+ <input type="hidden" name="target" .value=${this.postUrl} />
168
+ <input
169
+ type="url"
170
+ name="source"
171
+ placeholder="https://your-post-url.com"
172
+ required
173
+ part="input"
174
+ />
175
+ <button type="submit" part="button">Send</button>
176
+ </form>
177
+ </div>
178
+
179
+ <div class="list" part="list">
180
+ ${this.loading
181
+ ? html`<p class="status" part="status">Loading mentions…</p>`
182
+ : this.error
183
+ ? nothing
184
+ : this.mentions.length === 0
185
+ ? html`<p class="status" part="status">No mentions yet.</p>`
186
+ : html`
187
+ ${this.likes.length > 0 || this.reposts.length > 0
188
+ ? html`
189
+ <div class="reactions" part="reactions">
190
+ ${this.likes.length > 0
191
+ ? html`<span class="stat" part="stat">
192
+ <slot name="like-icon">♥</slot>
193
+ ${this.likes.length} like${this.likes.length !== 1 ? "s" : ""}
194
+ </span>`
195
+ : nothing}
196
+ ${this.reposts.length > 0
197
+ ? html`<span class="stat" part="stat">
198
+ <slot name="repost-icon">↩</slot>
199
+ ${this.reposts.length} repost${this.reposts.length !== 1 ? "s" : ""}
200
+ </span>`
201
+ : nothing}
202
+ </div>
203
+ `
204
+ : nothing}
205
+ ${this.replies.length > 0
206
+ ? html`<ol class="replies" part="replies">
207
+ ${this.replies.map((m) => this.renderReply(m))}
208
+ </ol>`
209
+ : nothing}
210
+ `}
211
+ </div>
212
+ </section>
213
+ `;
214
+ }
215
+
216
+ static styles = css`
217
+ :host {
218
+ --wm-text-color: inherit;
219
+ --wm-accent-color: #2563eb;
220
+ --wm-border-color: #6b7280;
221
+ --wm-reply-bg: transparent;
222
+ --wm-reply-border-color: #d1d5db;
223
+ --wm-input-bg: #ffffff;
224
+ --wm-input-border-color: #9ca3af;
225
+ --wm-avatar-bg: #9ca3af;
226
+ --wm-button-text-color: #ffffff;
227
+
228
+ display: block;
229
+ color: var(--wm-text-color);
230
+ margin-top: 3rem;
231
+ padding-top: 2rem;
232
+ border-top: 1px solid var(--wm-border-color);
233
+ }
234
+
235
+ h2 { margin: 0 0 1.5rem; }
236
+
237
+ .send p {
238
+ font-size: 0.9rem;
239
+ opacity: 0.75;
240
+ margin: 0 0 0.5rem;
241
+ }
242
+
243
+ form {
244
+ display: flex;
245
+ gap: 0.5rem;
246
+ flex-wrap: wrap;
247
+ margin-bottom: 2rem;
248
+ }
249
+
250
+ input[type="url"] {
251
+ flex: 1;
252
+ min-width: 0;
253
+ padding: 0.4rem 0.75rem;
254
+ border: 1px solid var(--wm-input-border-color);
255
+ border-radius: 6px;
256
+ background: var(--wm-input-bg);
257
+ color: var(--wm-text-color);
258
+ font-size: 0.9rem;
259
+ font-family: inherit;
260
+ }
261
+
262
+ button {
263
+ padding: 0.4rem 1rem;
264
+ border-radius: 6px;
265
+ border: none;
266
+ background: var(--wm-accent-color);
267
+ color: var(--wm-button-text-color);
268
+ font-size: 0.9rem;
269
+ font-family: inherit;
270
+ cursor: pointer;
271
+ }
272
+
273
+ .status { font-size: 0.9rem; opacity: 0.6; }
274
+
275
+ .reactions { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
276
+ .stat { font-size: 0.9rem; opacity: 0.8; }
277
+
278
+ .replies {
279
+ list-style: none;
280
+ padding: 0;
281
+ margin: 0;
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 1.25rem;
285
+ }
286
+
287
+ .reply {
288
+ background: var(--wm-reply-bg);
289
+ border: 1px solid var(--wm-reply-border-color);
290
+ border-radius: 8px;
291
+ padding: 0.75rem 1rem;
292
+ }
293
+
294
+ .reply-meta {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 0.5rem;
298
+ flex-wrap: wrap;
299
+ margin-bottom: 0.4rem;
300
+ }
301
+
302
+ .avatar {
303
+ width: 28px;
304
+ height: 28px;
305
+ border-radius: 50%;
306
+ object-fit: cover;
307
+ flex-shrink: 0;
308
+ }
309
+
310
+ .avatar.placeholder {
311
+ display: inline-block;
312
+ background: var(--wm-avatar-bg);
313
+ }
314
+
315
+ .reply-author { font-size: 0.9rem; font-weight: 600; }
316
+ .reply-author a { color: var(--wm-accent-color); text-decoration: none; }
317
+
318
+ .reply-date { font-size: 0.8rem; opacity: 0.6; margin-left: auto; }
319
+
320
+ .reply-link { font-size: 0.85rem; color: var(--wm-accent-color); text-decoration: none; }
321
+
322
+ .reply-content {
323
+ margin: 0;
324
+ font-size: 0.9rem;
325
+ opacity: 0.85;
326
+ line-height: 1.5;
327
+ }
328
+ `;
329
+ }
330
+
331
+ declare global {
332
+ interface HTMLElementTagNameMap {
333
+ "webmention-feed": WebmentionFeed;
334
+ }
335
+ }