sequoia-cli 0.5.5 → 0.5.7
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/dist/components/sequoia-comments.js +201 -47
- package/dist/components/sequoia-subscribe.js +401 -104
- package/dist/index.js +212 -108
- package/package.json +4 -4
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sequoia
|
|
3
|
-
*
|
|
4
|
-
* A self-contained Web Component that lets users subscribe to a publication
|
|
5
|
-
* via the AT Protocol by creating a site.standard.graph.subscription record.
|
|
2
|
+
* Sequoia Web Components — AT Protocol-powered engagement components
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Self-contained Web Components for subscribing to publications and
|
|
5
|
+
* recommending documents via the AT Protocol.
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Both components share:
|
|
8
|
+
* - OAuth redirect flow via a hosted callback endpoint
|
|
9
|
+
* - DID caching in a cookie (primary) and localStorage (fallback)
|
|
10
|
+
* - A common visual style driven by CSS custom properties
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
* - publication-uri: Override the publication AT URI (optional)
|
|
15
|
-
* - callback-uri: Redirect URI after OAuth authentication (default: "https://sequoia.pub/subscribe")
|
|
16
|
-
* - button-type: Branding style — "sequoia" (default), "bluesky", "blacksky", "atmosphere", or "plain"
|
|
17
|
-
* - label: Override the subscribe button label text
|
|
18
|
-
* - unsubscribe-label: Override the unsubscribe button label text
|
|
19
|
-
* - hide: Set to "auto" to hide if no publication URI is detected
|
|
20
|
-
*
|
|
21
|
-
* CSS Custom Properties:
|
|
12
|
+
* CSS Custom Properties (apply to both components):
|
|
22
13
|
* - --sequoia-fg-color: Text color (default: #1f2937)
|
|
23
14
|
* - --sequoia-bg-color: Background color (default: #ffffff)
|
|
24
15
|
* - --sequoia-border-color: Border color (default: #e5e7eb)
|
|
@@ -26,12 +17,6 @@
|
|
|
26
17
|
* - --sequoia-secondary-color: Secondary text color (default: #6b7280)
|
|
27
18
|
* - --sequoia-border-radius: Border radius (default: 8px)
|
|
28
19
|
* - --sequoia-icon-display: Icon display mode (default: inline-block) — set to "none" to hide
|
|
29
|
-
*
|
|
30
|
-
* Events:
|
|
31
|
-
* - sequoia-subscribed: Fired when the subscription is created successfully.
|
|
32
|
-
* detail: { publicationUri: string, recordUri: string }
|
|
33
|
-
* - sequoia-subscribe-error: Fired when the subscription fails.
|
|
34
|
-
* detail: { message: string }
|
|
35
20
|
*/
|
|
36
21
|
|
|
37
22
|
// ============================================================================
|
|
@@ -50,7 +35,7 @@ const styles = `
|
|
|
50
35
|
box-sizing: border-box;
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
.sequoia-
|
|
38
|
+
.sequoia-button {
|
|
54
39
|
display: inline-flex;
|
|
55
40
|
align-items: center;
|
|
56
41
|
gap: 0.375rem;
|
|
@@ -67,16 +52,16 @@ const styles = `
|
|
|
67
52
|
font-family: inherit;
|
|
68
53
|
}
|
|
69
54
|
|
|
70
|
-
.sequoia-
|
|
55
|
+
.sequoia-button:hover:not(:disabled) {
|
|
71
56
|
background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black);
|
|
72
57
|
}
|
|
73
58
|
|
|
74
|
-
.sequoia-
|
|
59
|
+
.sequoia-button:disabled {
|
|
75
60
|
opacity: 0.6;
|
|
76
61
|
cursor: not-allowed;
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
.sequoia-
|
|
64
|
+
.sequoia-button svg {
|
|
80
65
|
display: var(--sequoia-icon-display, inline-block);
|
|
81
66
|
width: 1rem;
|
|
82
67
|
height: 1rem;
|
|
@@ -148,6 +133,47 @@ const BUTTON_TYPES = {
|
|
|
148
133
|
plain: { icon: "", subscribe: "Subscribe", unsubscribe: "Unsubscribe" },
|
|
149
134
|
};
|
|
150
135
|
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Recommend Icon Configuration
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
const HEART_PATH =
|
|
141
|
+
"M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z";
|
|
142
|
+
const HEART_ICON_OUTLINED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${HEART_PATH}"/></svg>`;
|
|
143
|
+
const HEART_ICON_FILLED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="${HEART_PATH}"/></svg>`;
|
|
144
|
+
|
|
145
|
+
const STAR_PATH =
|
|
146
|
+
"M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z";
|
|
147
|
+
const STAR_ICON_OUTLINED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="${STAR_PATH}"/></svg>`;
|
|
148
|
+
const STAR_ICON_FILLED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path d="${STAR_PATH}"/></svg>`;
|
|
149
|
+
|
|
150
|
+
const THUMBS_UP_RECT_PATH = "M1 21h4V9H1v12z";
|
|
151
|
+
const THUMBS_UP_HAND_PATH =
|
|
152
|
+
"M23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z";
|
|
153
|
+
const THUMBS_UP_ICON_OUTLINED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="${THUMBS_UP_RECT_PATH}" fill="currentColor"/><path d="${THUMBS_UP_HAND_PATH}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
|
|
154
|
+
const THUMBS_UP_ICON_FILLED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="${THUMBS_UP_RECT_PATH}"/><path d="${THUMBS_UP_HAND_PATH}"/></svg>`;
|
|
155
|
+
|
|
156
|
+
const RECOMMEND_ICON_TYPES = {
|
|
157
|
+
heart: {
|
|
158
|
+
icon: HEART_ICON_OUTLINED,
|
|
159
|
+
iconActioned: HEART_ICON_FILLED,
|
|
160
|
+
action: "Recommend",
|
|
161
|
+
unaction: "Unrecommend",
|
|
162
|
+
},
|
|
163
|
+
star: {
|
|
164
|
+
icon: STAR_ICON_OUTLINED,
|
|
165
|
+
iconActioned: STAR_ICON_FILLED,
|
|
166
|
+
action: "Recommend",
|
|
167
|
+
unaction: "Unrecommend",
|
|
168
|
+
},
|
|
169
|
+
"thumbs-up": {
|
|
170
|
+
icon: THUMBS_UP_ICON_OUTLINED,
|
|
171
|
+
iconActioned: THUMBS_UP_ICON_FILLED,
|
|
172
|
+
action: "Recommend",
|
|
173
|
+
unaction: "Unrecommend",
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
151
177
|
// ============================================================================
|
|
152
178
|
// DID Storage
|
|
153
179
|
// ============================================================================
|
|
@@ -161,6 +187,7 @@ function storeSubscriberDid(did) {
|
|
|
161
187
|
const expires = new Date(
|
|
162
188
|
Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
163
189
|
).toUTCString();
|
|
190
|
+
// biome-ignore lint/suspicious/noDocumentCookie: back-compat with older browsers
|
|
164
191
|
document.cookie = `sequoia_did=${encodeURIComponent(did)}; Expires=${expires}; Path=/; SameSite=Lax; Secure`;
|
|
165
192
|
} catch {
|
|
166
193
|
// Cookie write may fail in some embedded contexts
|
|
@@ -200,6 +227,7 @@ function getStoredSubscriberDid() {
|
|
|
200
227
|
*/
|
|
201
228
|
function clearSubscriberDid() {
|
|
202
229
|
try {
|
|
230
|
+
// biome-ignore lint/suspicious/noDocumentCookie: back-compat with older browsers
|
|
203
231
|
document.cookie =
|
|
204
232
|
"sequoia_did=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; SameSite=Lax; Secure";
|
|
205
233
|
} catch {
|
|
@@ -229,7 +257,7 @@ function consumeReturnParams() {
|
|
|
229
257
|
changed = true;
|
|
230
258
|
}
|
|
231
259
|
|
|
232
|
-
if (did
|
|
260
|
+
if (did?.startsWith("did:")) {
|
|
233
261
|
storeSubscriberDid(did);
|
|
234
262
|
url.searchParams.delete("sequoia_did");
|
|
235
263
|
changed = true;
|
|
@@ -287,7 +315,13 @@ async function fetchPublicationUri(origin) {
|
|
|
287
315
|
// SSR-safe base class - use HTMLElement in browser, empty class in Node.js
|
|
288
316
|
const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {};
|
|
289
317
|
|
|
290
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Abstract base class shared by SequoiaSubscribe and SequoiaRecommend.
|
|
320
|
+
* Handles shadow DOM setup, state management, the OAuth redirect flow,
|
|
321
|
+
* DID storage, and button rendering. Subclasses implement template methods
|
|
322
|
+
* to provide resource-specific behaviour.
|
|
323
|
+
*/
|
|
324
|
+
class SequoiaActionBase extends BaseElement {
|
|
291
325
|
constructor() {
|
|
292
326
|
super();
|
|
293
327
|
const shadow = this.attachShadow({ mode: "open" });
|
|
@@ -301,82 +335,104 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
301
335
|
wrapper.part = "container";
|
|
302
336
|
|
|
303
337
|
this.wrapper = wrapper;
|
|
304
|
-
this.
|
|
338
|
+
this.actioned = false;
|
|
305
339
|
this.state = { type: "idle" };
|
|
306
340
|
this.abortController = null;
|
|
307
341
|
this.render();
|
|
308
342
|
}
|
|
309
343
|
|
|
310
|
-
static get observedAttributes() {
|
|
311
|
-
return [
|
|
312
|
-
"publication-uri",
|
|
313
|
-
"callback-uri",
|
|
314
|
-
"label",
|
|
315
|
-
"unsubscribe-label",
|
|
316
|
-
"button-type",
|
|
317
|
-
"hide",
|
|
318
|
-
];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
connectedCallback() {
|
|
322
|
-
consumeReturnParams();
|
|
323
|
-
this.checkPublication();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
344
|
disconnectedCallback() {
|
|
327
345
|
this.abortController?.abort();
|
|
328
346
|
}
|
|
329
347
|
|
|
330
348
|
attributeChangedCallback() {
|
|
331
|
-
if (this.state.type === "error" || this.state.type === "no-
|
|
349
|
+
if (this.state.type === "error" || this.state.type === "no-resource") {
|
|
332
350
|
this.state = { type: "idle" };
|
|
333
351
|
}
|
|
334
352
|
this.render();
|
|
335
353
|
}
|
|
336
354
|
|
|
337
|
-
|
|
338
|
-
return this.getAttribute("publication-uri") ?? null;
|
|
339
|
-
}
|
|
355
|
+
// ── Shared getters ───────────────────────────────────────────────────────
|
|
340
356
|
|
|
341
357
|
get callbackUri() {
|
|
342
|
-
return this.getAttribute("callback-uri") ??
|
|
358
|
+
return this.getAttribute("callback-uri") ?? this.defaultCallbackUri;
|
|
343
359
|
}
|
|
344
360
|
|
|
345
|
-
get
|
|
346
|
-
return this.getAttribute("
|
|
361
|
+
get hide() {
|
|
362
|
+
return this.getAttribute("hide") === "auto";
|
|
347
363
|
}
|
|
348
364
|
|
|
349
|
-
|
|
350
|
-
|
|
365
|
+
// ── Template methods (override in subclasses) ────────────────────────────
|
|
366
|
+
|
|
367
|
+
/** @returns {string} Default callback URI when the attribute is absent */
|
|
368
|
+
get defaultCallbackUri() {
|
|
369
|
+
return "";
|
|
351
370
|
}
|
|
352
371
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return
|
|
372
|
+
/** @returns {string} Query-parameter name for the resource URI */
|
|
373
|
+
get resourceParam() {
|
|
374
|
+
return "resourceUri";
|
|
356
375
|
}
|
|
357
376
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Value of the `action` query-parameter used in the unaction redirect.
|
|
379
|
+
* @returns {string}
|
|
380
|
+
*/
|
|
381
|
+
get unactionValue() {
|
|
382
|
+
return "unaction";
|
|
361
383
|
}
|
|
362
384
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
385
|
+
/** @returns {string} Key in the /check response that signals the action was taken */
|
|
386
|
+
get actionedKey() {
|
|
387
|
+
return "actioned";
|
|
388
|
+
}
|
|
366
389
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
390
|
+
/** @returns {string} CustomEvent name dispatched on success */
|
|
391
|
+
get actionedEventName() {
|
|
392
|
+
return "sequoia-actioned";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @returns {string} CustomEvent name dispatched on error */
|
|
396
|
+
get errorEventName() {
|
|
397
|
+
return "sequoia-action-error";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** @returns {string} Fallback error message when the thrown value has no message */
|
|
401
|
+
get defaultErrorMessage() {
|
|
402
|
+
return "Action failed";
|
|
374
403
|
}
|
|
375
404
|
|
|
376
|
-
|
|
405
|
+
/** @returns {string} SVG string for the button icon */
|
|
406
|
+
getIcon() {
|
|
407
|
+
return "";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** @returns {string} Accessible label for the button (defaults to the visible label) */
|
|
411
|
+
getAriaLabel() {
|
|
412
|
+
return this.actioned
|
|
413
|
+
? (this.getUnactionLabel?.() ?? this.getDefaultUnactionLabel?.() ?? "")
|
|
414
|
+
: (this.label ?? this.getDefaultActionLabel?.() ?? "");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolve the resource URI for this action. May perform async network calls.
|
|
419
|
+
* @returns {Promise<string>}
|
|
420
|
+
*/
|
|
421
|
+
async resolveResourceUri() {
|
|
422
|
+
throw new Error("resolveResourceUri() must be implemented by subclass");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Shared logic ─────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Check whether the current user has already taken this action for the
|
|
429
|
+
* given resource URI. Updates this.actioned and re-renders on success.
|
|
430
|
+
* @param {string} resourceUri
|
|
431
|
+
*/
|
|
432
|
+
async checkStatusFor(resourceUri) {
|
|
377
433
|
try {
|
|
378
434
|
const checkUrl = new URL(`${this.callbackUri}/check`);
|
|
379
|
-
checkUrl.searchParams.set(
|
|
435
|
+
checkUrl.searchParams.set(this.resourceParam, resourceUri);
|
|
380
436
|
|
|
381
437
|
// Pass the stored DID so the server can check without a session cookie
|
|
382
438
|
const storedDid = getStoredSubscriberDid();
|
|
@@ -389,12 +445,12 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
389
445
|
});
|
|
390
446
|
if (!res.ok) return;
|
|
391
447
|
const data = await res.json();
|
|
392
|
-
if (data.
|
|
393
|
-
this.
|
|
448
|
+
if (data[this.actionedKey]) {
|
|
449
|
+
this.actioned = true;
|
|
394
450
|
this.render();
|
|
395
451
|
}
|
|
396
452
|
} catch {
|
|
397
|
-
// Ignore errors — show default
|
|
453
|
+
// Ignore errors — show default action button
|
|
398
454
|
}
|
|
399
455
|
}
|
|
400
456
|
|
|
@@ -403,11 +459,10 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
403
459
|
return;
|
|
404
460
|
}
|
|
405
461
|
|
|
406
|
-
//
|
|
407
|
-
if (this.
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
window.location.href = `${this.callbackUri}?publicationUri=${encodeURIComponent(publicationUri)}&action=unsubscribe`;
|
|
462
|
+
// Unaction: redirect to the full-page unaction flow
|
|
463
|
+
if (this.actioned) {
|
|
464
|
+
const resourceUri = await this.resolveResourceUri();
|
|
465
|
+
window.location.href = `${this.callbackUri}?${this.resourceParam}=${encodeURIComponent(resourceUri)}&action=${this.unactionValue}`;
|
|
411
466
|
return;
|
|
412
467
|
}
|
|
413
468
|
|
|
@@ -415,28 +470,27 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
415
470
|
this.render();
|
|
416
471
|
|
|
417
472
|
try {
|
|
418
|
-
const
|
|
419
|
-
this.publicationUri ?? (await fetchPublicationUri());
|
|
473
|
+
const resourceUri = await this.resolveResourceUri();
|
|
420
474
|
|
|
421
475
|
const response = await fetch(this.callbackUri, {
|
|
422
476
|
method: "POST",
|
|
423
477
|
headers: { "Content-Type": "application/json" },
|
|
424
478
|
credentials: "include",
|
|
425
479
|
referrerPolicy: "no-referrer-when-downgrade",
|
|
426
|
-
body: JSON.stringify({
|
|
480
|
+
body: JSON.stringify({ [this.resourceParam]: resourceUri }),
|
|
427
481
|
});
|
|
428
482
|
|
|
429
483
|
const data = await response.json();
|
|
430
484
|
|
|
431
485
|
if (response.status === 401 && data.authenticated === false) {
|
|
432
|
-
// Redirect to the hosted
|
|
486
|
+
// Redirect to the hosted action page to complete OAuth,
|
|
433
487
|
// passing the current page URL (without credentials) as returnTo.
|
|
434
|
-
const
|
|
488
|
+
const actionUrl = new URL(data.subscribeUrl);
|
|
435
489
|
const pageUrl = new URL(window.location.href);
|
|
436
490
|
pageUrl.username = "";
|
|
437
491
|
pageUrl.password = "";
|
|
438
|
-
|
|
439
|
-
window.location.href =
|
|
492
|
+
actionUrl.searchParams.set("returnTo", pageUrl.toString());
|
|
493
|
+
window.location.href = actionUrl.toString();
|
|
440
494
|
return;
|
|
441
495
|
}
|
|
442
496
|
|
|
@@ -454,27 +508,27 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
454
508
|
}
|
|
455
509
|
}
|
|
456
510
|
|
|
457
|
-
this.
|
|
511
|
+
this.actioned = true;
|
|
458
512
|
this.state = { type: "idle" };
|
|
459
513
|
this.render();
|
|
460
514
|
|
|
461
515
|
this.dispatchEvent(
|
|
462
|
-
new CustomEvent(
|
|
516
|
+
new CustomEvent(this.actionedEventName, {
|
|
463
517
|
bubbles: true,
|
|
464
518
|
composed: true,
|
|
465
|
-
detail: {
|
|
519
|
+
detail: { [this.resourceParam]: resourceUri, recordUri },
|
|
466
520
|
}),
|
|
467
521
|
);
|
|
468
522
|
} catch (error) {
|
|
469
523
|
if (this.state.type !== "loading") return;
|
|
470
524
|
|
|
471
525
|
const message =
|
|
472
|
-
error instanceof Error ? error.message :
|
|
526
|
+
error instanceof Error ? error.message : this.defaultErrorMessage;
|
|
473
527
|
this.state = { type: "error", message };
|
|
474
528
|
this.render();
|
|
475
529
|
|
|
476
530
|
this.dispatchEvent(
|
|
477
|
-
new CustomEvent(
|
|
531
|
+
new CustomEvent(this.errorEventName, {
|
|
478
532
|
bubbles: true,
|
|
479
533
|
composed: true,
|
|
480
534
|
detail: { message },
|
|
@@ -486,7 +540,7 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
486
540
|
render() {
|
|
487
541
|
const { type } = this.state;
|
|
488
542
|
|
|
489
|
-
if (type === "no-
|
|
543
|
+
if (type === "no-resource") {
|
|
490
544
|
if (this.hide) {
|
|
491
545
|
this.wrapper.innerHTML = "";
|
|
492
546
|
this.wrapper.style.display = "none";
|
|
@@ -495,15 +549,15 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
495
549
|
}
|
|
496
550
|
|
|
497
551
|
const isLoading = type === "loading";
|
|
498
|
-
const config = BUTTON_TYPES[this.buttonType] ?? BUTTON_TYPES.sequoia;
|
|
499
|
-
|
|
500
552
|
const icon = isLoading
|
|
501
553
|
? `<span class="sequoia-loading-spinner"></span>`
|
|
502
|
-
:
|
|
554
|
+
: this.getIcon();
|
|
555
|
+
|
|
556
|
+
const label = this.actioned
|
|
557
|
+
? (this.getUnactionLabel?.() ?? this.getDefaultUnactionLabel?.() ?? "")
|
|
558
|
+
: (this.label ?? this.getDefaultActionLabel?.() ?? "");
|
|
503
559
|
|
|
504
|
-
const
|
|
505
|
-
? (this.unsubscribeLabel ?? config.unsubscribe)
|
|
506
|
-
: (this.label ?? config.subscribe);
|
|
560
|
+
const ariaLabel = this.getAriaLabel();
|
|
507
561
|
|
|
508
562
|
const errorHtml =
|
|
509
563
|
type === "error"
|
|
@@ -512,11 +566,11 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
512
566
|
|
|
513
567
|
this.wrapper.innerHTML = `
|
|
514
568
|
<button
|
|
515
|
-
class="sequoia-
|
|
569
|
+
class="sequoia-button"
|
|
516
570
|
type="button"
|
|
517
571
|
part="button"
|
|
518
572
|
${isLoading ? "disabled" : ""}
|
|
519
|
-
aria-label="${
|
|
573
|
+
aria-label="${ariaLabel}"
|
|
520
574
|
>
|
|
521
575
|
${icon}
|
|
522
576
|
${label}
|
|
@@ -529,6 +583,197 @@ class SequoiaSubscribe extends BaseElement {
|
|
|
529
583
|
}
|
|
530
584
|
}
|
|
531
585
|
|
|
586
|
+
class SequoiaSubscribe extends SequoiaActionBase {
|
|
587
|
+
static get observedAttributes() {
|
|
588
|
+
return [
|
|
589
|
+
"publication-uri",
|
|
590
|
+
"callback-uri",
|
|
591
|
+
"label",
|
|
592
|
+
"unsubscribe-label",
|
|
593
|
+
"button-type",
|
|
594
|
+
"hide",
|
|
595
|
+
];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
connectedCallback() {
|
|
599
|
+
consumeReturnParams();
|
|
600
|
+
this.checkPublication();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
get publicationUri() {
|
|
604
|
+
return this.getAttribute("publication-uri") ?? null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
get label() {
|
|
608
|
+
return this.getAttribute("label") ?? null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
get buttonType() {
|
|
612
|
+
const val = this.getAttribute("button-type");
|
|
613
|
+
return val && val in BUTTON_TYPES ? val : "sequoia";
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
get unsubscribeLabel() {
|
|
617
|
+
return this.getAttribute("unsubscribe-label") ?? null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── Template method overrides ────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
get defaultCallbackUri() {
|
|
623
|
+
return "https://sequoia.pub/subscribe";
|
|
624
|
+
}
|
|
625
|
+
get resourceParam() {
|
|
626
|
+
return "publicationUri";
|
|
627
|
+
}
|
|
628
|
+
get unactionValue() {
|
|
629
|
+
return "unsubscribe";
|
|
630
|
+
}
|
|
631
|
+
get actionedKey() {
|
|
632
|
+
return "subscribed";
|
|
633
|
+
}
|
|
634
|
+
get actionedEventName() {
|
|
635
|
+
return "sequoia-subscribed";
|
|
636
|
+
}
|
|
637
|
+
get errorEventName() {
|
|
638
|
+
return "sequoia-subscribe-error";
|
|
639
|
+
}
|
|
640
|
+
get defaultErrorMessage() {
|
|
641
|
+
return "Failed to subscribe";
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
getDefaultActionLabel() {
|
|
645
|
+
return (BUTTON_TYPES[this.buttonType] ?? BUTTON_TYPES.sequoia).subscribe;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
getDefaultUnactionLabel() {
|
|
649
|
+
return (BUTTON_TYPES[this.buttonType] ?? BUTTON_TYPES.sequoia).unsubscribe;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
getUnactionLabel() {
|
|
653
|
+
return this.unsubscribeLabel;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
getIcon() {
|
|
657
|
+
return (BUTTON_TYPES[this.buttonType] ?? BUTTON_TYPES.sequoia).icon;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async resolveResourceUri() {
|
|
661
|
+
return this.publicationUri ?? (await fetchPublicationUri());
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ── SequoiaSubscribe-specific logic ──────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
/** @returns {boolean} Whether the user is currently subscribed. Alias for this.actioned. */
|
|
667
|
+
get subscribed() {
|
|
668
|
+
return this.actioned;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Check whether the current user is subscribed to the given publication URI.
|
|
673
|
+
* Forwards to the shared checkStatusFor() method.
|
|
674
|
+
* @param {string} publicationUri
|
|
675
|
+
*/
|
|
676
|
+
checkSubscription(publicationUri) {
|
|
677
|
+
return this.checkStatusFor(publicationUri);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async checkPublication() {
|
|
681
|
+
this.abortController?.abort();
|
|
682
|
+
this.abortController = new AbortController();
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const uri = await this.resolveResourceUri();
|
|
686
|
+
this.checkStatusFor(uri);
|
|
687
|
+
} catch {
|
|
688
|
+
this.state = { type: "no-resource" };
|
|
689
|
+
this.render();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
class SequoiaRecommend extends SequoiaActionBase {
|
|
695
|
+
static get observedAttributes() {
|
|
696
|
+
return ["document-uri", "callback-uri", "button-type", "hide"];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
connectedCallback() {
|
|
700
|
+
consumeReturnParams();
|
|
701
|
+
this.checkDocument();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
get documentUri() {
|
|
705
|
+
const attrUri = this.getAttribute("document-uri");
|
|
706
|
+
if (attrUri) return attrUri;
|
|
707
|
+
const linkTag = document.querySelector(
|
|
708
|
+
'link[rel="site.standard.document"]',
|
|
709
|
+
);
|
|
710
|
+
return linkTag?.href ?? null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
get buttonType() {
|
|
714
|
+
const val = this.getAttribute("button-type");
|
|
715
|
+
return val && val in RECOMMEND_ICON_TYPES ? val : "heart";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── Template method overrides ────────────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
get defaultCallbackUri() {
|
|
721
|
+
return "https://sequoia.pub/recommend";
|
|
722
|
+
}
|
|
723
|
+
get resourceParam() {
|
|
724
|
+
return "documentUri";
|
|
725
|
+
}
|
|
726
|
+
get unactionValue() {
|
|
727
|
+
return "remove";
|
|
728
|
+
}
|
|
729
|
+
get actionedKey() {
|
|
730
|
+
return "recommended";
|
|
731
|
+
}
|
|
732
|
+
get actionedEventName() {
|
|
733
|
+
return "sequoia-recommended";
|
|
734
|
+
}
|
|
735
|
+
get errorEventName() {
|
|
736
|
+
return "sequoia-recommend-error";
|
|
737
|
+
}
|
|
738
|
+
get defaultErrorMessage() {
|
|
739
|
+
return "Failed to recommend";
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
getAriaLabel() {
|
|
743
|
+
const config =
|
|
744
|
+
RECOMMEND_ICON_TYPES[this.buttonType] ?? RECOMMEND_ICON_TYPES.heart;
|
|
745
|
+
return this.actioned ? config.unaction : config.action;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
getIcon() {
|
|
749
|
+
const config =
|
|
750
|
+
RECOMMEND_ICON_TYPES[this.buttonType] ?? RECOMMEND_ICON_TYPES.heart;
|
|
751
|
+
return this.actioned ? config.iconActioned : config.icon;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async resolveResourceUri() {
|
|
755
|
+
const uri = this.documentUri;
|
|
756
|
+
if (!uri) throw new Error("No document URI found");
|
|
757
|
+
return uri;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── SequoiaRecommend-specific logic ──────────────────────────────────────
|
|
761
|
+
|
|
762
|
+
async checkDocument() {
|
|
763
|
+
this.abortController?.abort();
|
|
764
|
+
this.abortController = new AbortController();
|
|
765
|
+
|
|
766
|
+
const uri = this.documentUri;
|
|
767
|
+
if (!uri) {
|
|
768
|
+
this.state = { type: "no-resource" };
|
|
769
|
+
this.render();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
this.checkStatusFor(uri);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
532
777
|
/**
|
|
533
778
|
* Escape HTML special characters (no DOM dependency for SSR).
|
|
534
779
|
* @param {string} text
|
|
@@ -542,10 +787,62 @@ function escapeHtml(text) {
|
|
|
542
787
|
.replace(/"/g, """);
|
|
543
788
|
}
|
|
544
789
|
|
|
545
|
-
// Register the custom
|
|
790
|
+
// Register the custom elements
|
|
546
791
|
if (typeof customElements !== "undefined") {
|
|
547
792
|
customElements.define("sequoia-subscribe", SequoiaSubscribe);
|
|
793
|
+
customElements.define("sequoia-recommend", SequoiaRecommend);
|
|
548
794
|
}
|
|
549
795
|
|
|
550
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Sequoia Subscribe - An AT Protocol-powered subscribe component
|
|
798
|
+
*
|
|
799
|
+
* A self-contained Web Component that lets users subscribe to a publication
|
|
800
|
+
* via the AT Protocol by creating a site.standard.graph.subscription record.
|
|
801
|
+
*
|
|
802
|
+
* Usage:
|
|
803
|
+
* <sequoia-subscribe></sequoia-subscribe>
|
|
804
|
+
*
|
|
805
|
+
* The component resolves the publication AT URI from the host site's
|
|
806
|
+
* /.well-known/site.standard.publication endpoint.
|
|
807
|
+
*
|
|
808
|
+
* Attributes:
|
|
809
|
+
* - publication-uri: Override the publication AT URI (optional)
|
|
810
|
+
* - callback-uri: Redirect URI after OAuth authentication (default: "https://sequoia.pub/subscribe")
|
|
811
|
+
* - button-type: Branding style — "sequoia" (default), "bluesky", "blacksky", "atmosphere", or "plain"
|
|
812
|
+
* - label: Override the subscribe button label text
|
|
813
|
+
* - unsubscribe-label: Override the unsubscribe button label text
|
|
814
|
+
* - hide: Set to "auto" to hide if no publication URI is detected
|
|
815
|
+
*
|
|
816
|
+
* Events:
|
|
817
|
+
* - sequoia-subscribed: Fired when the subscription is created successfully.
|
|
818
|
+
* detail: { publicationUri: string, recordUri: string }
|
|
819
|
+
* - sequoia-subscribe-error: Fired when the subscription fails.
|
|
820
|
+
* detail: { message: string }
|
|
821
|
+
*/
|
|
551
822
|
export { SequoiaSubscribe };
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Sequoia Recommend - An AT Protocol-powered recommend component
|
|
826
|
+
*
|
|
827
|
+
* A self-contained Web Component that lets users recommend a document
|
|
828
|
+
* via the AT Protocol by creating a site.standard.graph.recommend record.
|
|
829
|
+
*
|
|
830
|
+
* Usage:
|
|
831
|
+
* <sequoia-recommend></sequoia-recommend>
|
|
832
|
+
*
|
|
833
|
+
* The component resolves the document AT URI from the `document-uri` attribute
|
|
834
|
+
* or a <link rel="site.standard.document" href="at://..."> tag in the page head.
|
|
835
|
+
*
|
|
836
|
+
* Attributes:
|
|
837
|
+
* - document-uri: AT Protocol URI of the document to recommend (optional if link tag present)
|
|
838
|
+
* - callback-uri: Redirect URI after OAuth authentication (default: "https://sequoia.pub/recommend")
|
|
839
|
+
* - button-type: Icon style — "heart" (default), "star", or "thumbs-up"
|
|
840
|
+
* - hide: Set to "auto" to hide if no document URI is detected
|
|
841
|
+
*
|
|
842
|
+
* Events:
|
|
843
|
+
* - sequoia-recommended: Fired when the recommendation is created successfully.
|
|
844
|
+
* detail: { documentUri: string, recordUri: string }
|
|
845
|
+
* - sequoia-recommend-error: Fired when the recommendation fails.
|
|
846
|
+
* detail: { message: string }
|
|
847
|
+
*/
|
|
848
|
+
export { SequoiaRecommend };
|