sequoia-cli 0.5.0-alpha.0 → 0.5.1
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 +53 -16
- package/dist/components/sequoia-subscribe.js +551 -0
- package/dist/index.js +4484 -721
- package/package.json +8 -3
|
@@ -113,21 +113,34 @@ const styles = `
|
|
|
113
113
|
align-items: center;
|
|
114
114
|
gap: 0.375rem;
|
|
115
115
|
padding: 0.5rem 1rem;
|
|
116
|
-
background: var(--sequoia-accent-color, #2563eb);
|
|
117
|
-
color: #ffffff;
|
|
118
116
|
border: none;
|
|
119
|
-
border-radius: var(--sequoia-border-radius,
|
|
117
|
+
border-radius: var(--sequoia-border-radius, 15px);
|
|
120
118
|
font-size: 0.875rem;
|
|
121
119
|
font-weight: 500;
|
|
122
120
|
cursor: pointer;
|
|
123
121
|
text-decoration: none;
|
|
124
122
|
transition: background-color 0.15s ease;
|
|
123
|
+
margin-left:10px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.sequoia-reply-bluesky {
|
|
127
|
+
background: var(--sequoia-accent-color, #2563eb);
|
|
128
|
+
color: #ffffff;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
.sequoia-reply-
|
|
131
|
+
.sequoia-reply-blacksky {
|
|
132
|
+
background: var(--sequoia-accent-color, #6060E9);
|
|
133
|
+
color: #ffffff;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sequoia-reply-bluesky:hover {
|
|
128
137
|
background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black);
|
|
129
138
|
}
|
|
130
139
|
|
|
140
|
+
.sequoia-reply-blacksky:hover {
|
|
141
|
+
background: color-mix(in srgb, var(--sequoia-accent-color, #5252c3) 85%, black);
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
.sequoia-reply-button svg {
|
|
132
145
|
width: 1rem;
|
|
133
146
|
height: 1rem;
|
|
@@ -547,6 +560,20 @@ function buildBskyAppUrl(postUri) {
|
|
|
547
560
|
return `https://bsky.app/profile/${parsed.did}/post/${parsed.rkey}`;
|
|
548
561
|
}
|
|
549
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Build a Blacksky app URL for a post
|
|
565
|
+
* @param {string} postUri - AT Protocol URI for the post
|
|
566
|
+
* @returns {string} Blacksky app URL
|
|
567
|
+
*/
|
|
568
|
+
function buildBlackskyAppUrl(postUri) {
|
|
569
|
+
const parsed = parseAtUri(postUri);
|
|
570
|
+
if (!parsed) {
|
|
571
|
+
throw new Error(`Invalid post URI: ${postUri}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return `https://blacksky.community/profile/${parsed.did}/post/${parsed.rkey}`;
|
|
575
|
+
}
|
|
576
|
+
|
|
550
577
|
/**
|
|
551
578
|
* Type guard for ThreadViewPost
|
|
552
579
|
* @param {any} post - Post to check
|
|
@@ -563,6 +590,8 @@ function isThreadViewPost(post) {
|
|
|
563
590
|
const BLUESKY_ICON = `<svg class="sequoia-bsky-logo" viewBox="0 0 600 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
564
591
|
<path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/>
|
|
565
592
|
</svg>`;
|
|
593
|
+
const BLACKSKY_ICON =
|
|
594
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.0620117 0.348442 87.9941 74.9653"><path d="M41.9565 74.9643L24.0161 74.9653L41.9565 74.9643ZM63.8511 74.9653H45.9097L63.8501 74.9643V57.3286H63.8511V74.9653ZM45.9097 44.5893C45.9099 49.2737 49.7077 53.0707 54.3921 53.0707H63.8501V57.3286H54.3921C49.7077 57.3286 45.9099 61.1257 45.9097 65.81V74.9643H41.9565V65.81C41.9563 61.1258 38.1593 57.3287 33.4751 57.3286H24.0161V53.0707H33.4741C38.1587 53.0707 41.9565 49.2729 41.9565 44.5883V35.1303H45.9097V44.5893ZM63.8511 53.0707H63.8501V35.1303H63.8511V53.0707Z" fill="white"></path><path d="M52.7272 9.83198C49.4148 13.1445 49.4148 18.5151 52.7272 21.8275L59.4155 28.5158L56.4051 31.5262L49.7169 24.8379C46.4044 21.5254 41.0338 21.5254 37.7213 24.8379L31.2482 31.3111L28.4527 28.5156L34.9259 22.0424C38.2383 18.7299 38.2383 13.3594 34.9259 10.0469L28.2378 3.35883L31.2482 0.348442L37.9365 7.03672C41.2489 10.3492 46.6195 10.3492 49.932 7.03672L56.6203 0.348442L59.4155 3.14371L52.7272 9.83198Z" fill="white"/><path d="M24.3831 23.2335C23.1706 27.7584 25.8559 32.4095 30.3808 33.6219L39.5172 36.07L38.4154 40.182L29.2793 37.734C24.7544 36.5215 20.1033 39.2068 18.8909 43.7317L16.5215 52.5745L12.7028 51.5513L15.0721 42.7088C16.2846 38.1839 13.5993 33.5328 9.07434 32.3204L-0.0620117 29.8723L1.03987 25.76L10.1762 28.2081C14.7011 29.4206 19.3522 26.7352 20.5647 22.2103L23.0127 13.074L26.8311 14.0971L24.3831 23.2335Z" fill="white"/><path d="M67.3676 22.0297C68.5801 26.5546 73.2311 29.2399 77.756 28.0275L86.8923 25.5794L87.9941 29.6914L78.8578 32.1394C74.3329 33.3519 71.6476 38.003 72.86 42.5279L75.2294 51.3707L71.411 52.3938L69.0417 43.5513C67.8293 39.0264 63.1782 36.3411 58.6533 37.5535L49.5169 40.0016L48.415 35.8894L57.5514 33.4413C62.0763 32.2288 64.7616 27.5778 63.5492 23.0528L61.1011 13.9165L64.9195 12.8934L67.3676 22.0297Z" fill="white"/></svg>';
|
|
566
595
|
|
|
567
596
|
// ============================================================================
|
|
568
597
|
// Web Component
|
|
@@ -588,7 +617,6 @@ class SequoiaComments extends BaseElement {
|
|
|
588
617
|
this.commentsContainer = container;
|
|
589
618
|
this.state = { type: "loading" };
|
|
590
619
|
this.abortController = null;
|
|
591
|
-
|
|
592
620
|
}
|
|
593
621
|
|
|
594
622
|
static get observedAttributes() {
|
|
@@ -661,6 +689,7 @@ class SequoiaComments extends BaseElement {
|
|
|
661
689
|
}
|
|
662
690
|
|
|
663
691
|
const postUrl = buildBskyAppUrl(document.bskyPostRef.uri);
|
|
692
|
+
const blackskyPostUrl = buildBlackskyAppUrl(document.bskyPostRef.uri);
|
|
664
693
|
|
|
665
694
|
// Fetch the post thread
|
|
666
695
|
const thread = await getPostThread(document.bskyPostRef.uri, this.depth);
|
|
@@ -668,12 +697,12 @@ class SequoiaComments extends BaseElement {
|
|
|
668
697
|
// Check if there are any replies
|
|
669
698
|
const replies = thread.replies?.filter(isThreadViewPost) ?? [];
|
|
670
699
|
if (replies.length === 0) {
|
|
671
|
-
this.state = { type: "empty", postUrl };
|
|
700
|
+
this.state = { type: "empty", postUrl, blackskyPostUrl };
|
|
672
701
|
this.render();
|
|
673
702
|
return;
|
|
674
703
|
}
|
|
675
704
|
|
|
676
|
-
this.state = { type: "loaded", thread, postUrl };
|
|
705
|
+
this.state = { type: "loaded", thread, postUrl, blackskyPostUrl };
|
|
677
706
|
this.render();
|
|
678
707
|
} catch (error) {
|
|
679
708
|
const message =
|
|
@@ -701,7 +730,7 @@ class SequoiaComments extends BaseElement {
|
|
|
701
730
|
</div>
|
|
702
731
|
`;
|
|
703
732
|
if (this.hide) {
|
|
704
|
-
this.commentsContainer.style.display =
|
|
733
|
+
this.commentsContainer.style.display = "none";
|
|
705
734
|
}
|
|
706
735
|
break;
|
|
707
736
|
|
|
@@ -717,10 +746,14 @@ class SequoiaComments extends BaseElement {
|
|
|
717
746
|
this.commentsContainer.innerHTML = `
|
|
718
747
|
<div class="sequoia-comments-header">
|
|
719
748
|
<h3 class="sequoia-comments-title">Comments</h3>
|
|
720
|
-
<
|
|
721
|
-
${
|
|
722
|
-
|
|
723
|
-
|
|
749
|
+
<div>
|
|
750
|
+
<a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky">
|
|
751
|
+
${BLUESKY_ICON}
|
|
752
|
+
</a>
|
|
753
|
+
<a href="${this.state.blackskyPostUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky">
|
|
754
|
+
${BLACKSKY_ICON}
|
|
755
|
+
</a>
|
|
756
|
+
</div>
|
|
724
757
|
</div>
|
|
725
758
|
<div class="sequoia-empty">
|
|
726
759
|
No comments yet. Be the first to reply on Bluesky!
|
|
@@ -747,10 +780,14 @@ class SequoiaComments extends BaseElement {
|
|
|
747
780
|
this.commentsContainer.innerHTML = `
|
|
748
781
|
<div class="sequoia-comments-header">
|
|
749
782
|
<h3 class="sequoia-comments-title">${commentCount} Comment${commentCount !== 1 ? "s" : ""}</h3>
|
|
750
|
-
<
|
|
751
|
-
${
|
|
752
|
-
|
|
753
|
-
|
|
783
|
+
<div>
|
|
784
|
+
<a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky">
|
|
785
|
+
${BLUESKY_ICON}
|
|
786
|
+
</a>
|
|
787
|
+
<a href="${this.state.blackskyPostUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky">
|
|
788
|
+
${BLACKSKY_ICON}
|
|
789
|
+
</a>
|
|
790
|
+
</div>
|
|
754
791
|
</div>
|
|
755
792
|
<div class="sequoia-comments-list">
|
|
756
793
|
${threadsHtml}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequoia Subscribe - An AT Protocol-powered subscribe component
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <sequoia-subscribe></sequoia-subscribe>
|
|
9
|
+
*
|
|
10
|
+
* The component resolves the publication AT URI from the host site's
|
|
11
|
+
* /.well-known/site.standard.publication endpoint.
|
|
12
|
+
*
|
|
13
|
+
* Attributes:
|
|
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:
|
|
22
|
+
* - --sequoia-fg-color: Text color (default: #1f2937)
|
|
23
|
+
* - --sequoia-bg-color: Background color (default: #ffffff)
|
|
24
|
+
* - --sequoia-border-color: Border color (default: #e5e7eb)
|
|
25
|
+
* - --sequoia-accent-color: Accent/button color (default: #2563eb)
|
|
26
|
+
* - --sequoia-secondary-color: Secondary text color (default: #6b7280)
|
|
27
|
+
* - --sequoia-border-radius: Border radius (default: 8px)
|
|
28
|
+
* - --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
|
+
*/
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Styles
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
const styles = `
|
|
42
|
+
:host {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
45
|
+
color: var(--sequoia-fg-color, #1f2937);
|
|
46
|
+
line-height: 1.5;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
* {
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sequoia-subscribe-button {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 0.375rem;
|
|
57
|
+
padding: 0.5rem 1rem;
|
|
58
|
+
background: var(--sequoia-accent-color, #2563eb);
|
|
59
|
+
color: #ffffff;
|
|
60
|
+
border: none;
|
|
61
|
+
border-radius: var(--sequoia-border-radius, 8px);
|
|
62
|
+
font-size: 0.875rem;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
transition: background-color 0.15s ease;
|
|
67
|
+
font-family: inherit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sequoia-subscribe-button:hover:not(:disabled) {
|
|
71
|
+
background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.sequoia-subscribe-button:disabled {
|
|
75
|
+
opacity: 0.6;
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.sequoia-subscribe-button svg {
|
|
80
|
+
display: var(--sequoia-icon-display, inline-block);
|
|
81
|
+
width: 1rem;
|
|
82
|
+
height: 1rem;
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sequoia-loading-spinner {
|
|
87
|
+
display: inline-block;
|
|
88
|
+
width: 1rem;
|
|
89
|
+
height: 1rem;
|
|
90
|
+
border: 2px solid rgba(255, 255, 255, 0.4);
|
|
91
|
+
border-top-color: #ffffff;
|
|
92
|
+
border-radius: 50%;
|
|
93
|
+
animation: sequoia-spin 0.8s linear infinite;
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@keyframes sequoia-spin {
|
|
98
|
+
to { transform: rotate(360deg); }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sequoia-error-message {
|
|
102
|
+
display: inline-block;
|
|
103
|
+
font-size: 0.8125rem;
|
|
104
|
+
color: #dc2626;
|
|
105
|
+
margin-top: 0.375rem;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Icons
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
const BLUESKY_ICON = `<svg class="sequoia-bsky-logo" viewBox="0 0 600 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
114
|
+
<path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/>
|
|
115
|
+
</svg>`;
|
|
116
|
+
|
|
117
|
+
const BLACKSKY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.0620117 0.348442 87.9941 74.9653" fill="currentColor"><path d="M41.9565 74.9643L24.0161 74.9653L41.9565 74.9643ZM63.8511 74.9653H45.9097L63.8501 74.9643V57.3286H63.8511V74.9653ZM45.9097 44.5893C45.9099 49.2737 49.7077 53.0707 54.3921 53.0707H63.8501V57.3286H54.3921C49.7077 57.3286 45.9099 61.1257 45.9097 65.81V74.9643H41.9565V65.81C41.9563 61.1258 38.1593 57.3287 33.4751 57.3286H24.0161V53.0707H33.4741C38.1587 53.0707 41.9565 49.2729 41.9565 44.5883V35.1303H45.9097V44.5893ZM63.8511 53.0707H63.8501V35.1303H63.8511V53.0707Z"/><path d="M52.7272 9.83198C49.4148 13.1445 49.4148 18.5151 52.7272 21.8275L59.4155 28.5158L56.4051 31.5262L49.7169 24.8379C46.4044 21.5254 41.0338 21.5254 37.7213 24.8379L31.2482 31.3111L28.4527 28.5156L34.9259 22.0424C38.2383 18.7299 38.2383 13.3594 34.9259 10.0469L28.2378 3.35883L31.2482 0.348442L37.9365 7.03672C41.2489 10.3492 46.6195 10.3492 49.932 7.03672L56.6203 0.348442L59.4155 3.14371L52.7272 9.83198Z"/><path d="M24.3831 23.2335C23.1706 27.7584 25.8559 32.4095 30.3808 33.6219L39.5172 36.07L38.4154 40.182L29.2793 37.734C24.7544 36.5215 20.1033 39.2068 18.8909 43.7317L16.5215 52.5745L12.7028 51.5513L15.0721 42.7088C16.2846 38.1839 13.5993 33.5328 9.07434 32.3204L-0.0620117 29.8723L1.03987 25.76L10.1762 28.2081C14.7011 29.4206 19.3522 26.7352 20.5647 22.2103L23.0127 13.074L26.8311 14.0971L24.3831 23.2335Z"/><path d="M67.3676 22.0297C68.5801 26.5546 73.2311 29.2399 77.756 28.0275L86.8923 25.5794L87.9941 29.6914L78.8578 32.1394C74.3329 33.3519 71.6476 38.003 72.86 42.5279L75.2294 51.3707L71.411 52.3938L69.0417 43.5513C67.8293 39.0264 63.1782 36.3411 58.6533 37.5535L49.5169 40.0016L48.415 35.8894L57.5514 33.4413C62.0763 32.2288 64.7616 27.5778 63.5492 23.0528L61.1011 13.9165L64.9195 12.8934L67.3676 22.0297Z"/></svg>`;
|
|
118
|
+
|
|
119
|
+
const SEQUOIA_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 151" fill="none" stroke="currentColor" stroke-width="10.5" stroke-linecap="round" stroke-linejoin="round"><path d="M47.25 145.217V54.2167M68.25 111.596C74.6356 107.909 79.9382 102.606 83.6245 96.2201C87.3108 89.8341 89.251 82.5902 89.25 75.2167C89.2641 64.2875 85.0033 53.7863 77.378 45.9567C78.8172 41.2475 79.1324 36.2663 78.2981 31.4132C77.4638 26.5601 75.5033 21.9701 72.574 18.0118C69.6448 14.0535 65.8283 10.8371 61.4309 8.62081C57.0335 6.4045 52.1778 5.25 47.2535 5.25C42.3292 5.25 37.4734 6.4045 33.0761 8.62081C28.6787 10.8371 24.8622 14.0535 21.9329 18.0118C19.0037 21.9701 17.0432 26.5601 16.2089 31.4132C15.3746 36.2663 15.6897 41.2475 17.129 45.9567C9.50114 53.7851 5.23776 64.2866 5.25003 75.2167C5.25003 90.7567 13.699 104.337 26.25 111.596M47.25 96.2167L64.75 78.7167M47.25 82.2167L29.75 64.7167M33.25 145.217H61.25"/></svg>`;
|
|
120
|
+
|
|
121
|
+
const ATMOSPHERE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 114 114" fill="currentColor"><path d="M56.9119 114C48.655 114 41.0566 112.632 34.1167 109.896C27.1769 107.16 21.1488 103.284 16.0326 98.268C10.9125 93.2969 6.87661 87.3195 4.17911 80.712C1.39304 73.9733 0 66.652 0 58.748C0 49.324 1.44369 40.964 4.33108 33.668C7.26912 26.372 11.3216 20.2413 16.4885 15.276C21.6743 10.2798 27.829 6.39999 34.5726 3.876C41.4618 1.292 48.8322 0 56.6839 0C66.3085 0 74.7427 1.49467 81.9865 4.484C89.2303 7.47333 95.2583 11.5267 100.071 16.644C104.833 21.6798 108.483 27.6613 110.784 34.2C113.115 40.736 114.178 47.576 113.976 54.72C113.722 64.5493 111.671 72.0986 107.821 77.368C103.971 82.5866 97.9938 85.196 89.8888 85.196C85.7259 85.2315 81.6218 84.2118 77.9594 82.232C74.4587 80.3688 71.7969 77.2446 70.513 73.492L74.92 73.72C72.8431 77.6213 70.0064 80.3573 66.4098 81.928C62.9411 83.4714 59.1886 84.2738 55.3922 84.284C50.2759 84.284 45.7676 83.1946 41.8671 81.016C37.9939 78.8144 34.8103 75.5775 32.673 71.668C30.4442 67.6653 29.3297 63.0293 29.3297 57.76C29.3297 52.3387 30.4948 47.652 32.825 43.7C35.0512 39.7998 38.3119 36.5909 42.247 34.428C46.1981 32.2493 50.6559 31.16 55.6201 31.16C58.9128 31.16 62.332 31.844 65.8779 33.212C69.4745 34.58 72.2606 36.5053 74.2362 38.988L71.1208 42.94V33.288H81.3027L81.0747 60.572C81.0747 64.4733 81.8345 67.412 83.3542 69.388C84.8739 71.364 87.1281 72.352 90.1168 72.352C92.7509 72.352 94.7771 71.6173 96.1955 70.148C97.6645 68.628 98.6776 66.576 99.2348 63.992C99.8841 61.0707 100.24 58.092 100.299 55.1C100.451 47.2467 99.2855 40.6347 96.8033 35.264C94.3212 29.8933 90.9526 25.5613 86.6975 22.268C82.5822 18.9703 77.857 16.5168 72.7925 15.048C67.7269 13.528 62.6866 12.768 57.6717 12.768C50.5799 12.768 44.2732 13.908 38.7517 16.188C33.2302 18.4173 28.5699 21.584 24.7707 25.688C21.0222 29.7413 18.1855 34.5547 16.2605 40.128C14.3863 45.6507 13.4998 51.7307 13.6011 58.368C13.8037 64.9547 14.9941 70.8826 17.1723 76.152C19.2339 81.2487 22.3399 85.857 26.2904 89.68C30.2557 93.4697 34.9649 96.3941 40.1194 98.268C45.4383 100.244 51.2637 101.232 57.5957 101.232C61.1416 101.232 64.6622 100.827 68.1575 100.016C71.7034 99.256 74.9453 98.1666 77.8834 96.748L82.2145 108.604C78.314 110.428 74.2108 111.771 69.9051 112.632C65.6345 113.546 61.2791 114.004 56.9119 114ZM56.304 71.364C59.9006 71.364 62.9146 70.3253 65.3461 68.248C67.7775 66.1706 68.9933 62.6493 68.9933 57.684C68.9933 53.1747 67.9042 49.78 65.726 47.5C63.5984 45.1693 60.5844 44.004 56.6839 44.004C52.0742 44.004 48.6296 45.22 46.3501 47.652C44.0706 50.084 42.9308 53.428 42.9308 57.684C42.9308 62.0413 44.0959 65.4106 46.4261 67.792C48.8069 70.1733 52.0996 71.364 56.304 71.364Z"/></svg>`;
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Button Type Configuration
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
const BUTTON_TYPES = {
|
|
128
|
+
sequoia: {
|
|
129
|
+
icon: SEQUOIA_ICON,
|
|
130
|
+
subscribe: "Subscribe on Sequoia",
|
|
131
|
+
unsubscribe: "Unsubscribe",
|
|
132
|
+
},
|
|
133
|
+
bluesky: {
|
|
134
|
+
icon: BLUESKY_ICON,
|
|
135
|
+
subscribe: "Subscribe on Bluesky",
|
|
136
|
+
unsubscribe: "Unsubscribe",
|
|
137
|
+
},
|
|
138
|
+
blacksky: {
|
|
139
|
+
icon: BLACKSKY_ICON,
|
|
140
|
+
subscribe: "Subscribe on Blacksky",
|
|
141
|
+
unsubscribe: "Unsubscribe",
|
|
142
|
+
},
|
|
143
|
+
atmosphere: {
|
|
144
|
+
icon: ATMOSPHERE_ICON,
|
|
145
|
+
subscribe: "Subscribe on Atmosphere",
|
|
146
|
+
unsubscribe: "Unsubscribe",
|
|
147
|
+
},
|
|
148
|
+
plain: { icon: "", subscribe: "Subscribe", unsubscribe: "Unsubscribe" },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// DID Storage
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Store the subscriber DID. Tries a cookie first; falls back to localStorage.
|
|
157
|
+
* @param {string} did
|
|
158
|
+
*/
|
|
159
|
+
function storeSubscriberDid(did) {
|
|
160
|
+
try {
|
|
161
|
+
const expires = new Date(
|
|
162
|
+
Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
163
|
+
).toUTCString();
|
|
164
|
+
document.cookie = `sequoia_did=${encodeURIComponent(did)}; expires=${expires}; path=/; SameSite=Lax`;
|
|
165
|
+
} catch {
|
|
166
|
+
// Cookie write may fail in some embedded contexts
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
localStorage.setItem("sequoia_did", did);
|
|
170
|
+
} catch {
|
|
171
|
+
// localStorage may be unavailable
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Retrieve the stored subscriber DID. Checks cookie first, then localStorage.
|
|
177
|
+
* @returns {string | null}
|
|
178
|
+
*/
|
|
179
|
+
function getStoredSubscriberDid() {
|
|
180
|
+
try {
|
|
181
|
+
const match = document.cookie.match(/(?:^|;\s*)sequoia_did=([^;]+)/);
|
|
182
|
+
if (match) {
|
|
183
|
+
const did = decodeURIComponent(match[1]);
|
|
184
|
+
if (did.startsWith("did:")) return did;
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// ignore
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const did = localStorage.getItem("sequoia_did");
|
|
191
|
+
if (did?.startsWith("did:")) return did;
|
|
192
|
+
} catch {
|
|
193
|
+
// ignore
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove the stored subscriber DID from both cookie and localStorage.
|
|
200
|
+
*/
|
|
201
|
+
function clearSubscriberDid() {
|
|
202
|
+
try {
|
|
203
|
+
document.cookie =
|
|
204
|
+
"sequoia_did=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax";
|
|
205
|
+
} catch {
|
|
206
|
+
// ignore
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
localStorage.removeItem("sequoia_did");
|
|
210
|
+
} catch {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check the current page URL for sequoia_did / sequoia_unsubscribed params
|
|
217
|
+
* set by the subscribe redirect flow. Consumes them by removing from the URL.
|
|
218
|
+
*/
|
|
219
|
+
function consumeReturnParams() {
|
|
220
|
+
const url = new URL(window.location.href);
|
|
221
|
+
const did = url.searchParams.get("sequoia_did");
|
|
222
|
+
const unsubscribed = url.searchParams.get("sequoia_unsubscribed");
|
|
223
|
+
|
|
224
|
+
let changed = false;
|
|
225
|
+
|
|
226
|
+
if (unsubscribed === "1") {
|
|
227
|
+
clearSubscriberDid();
|
|
228
|
+
url.searchParams.delete("sequoia_unsubscribed");
|
|
229
|
+
changed = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (did && did.startsWith("did:")) {
|
|
233
|
+
storeSubscriberDid(did);
|
|
234
|
+
url.searchParams.delete("sequoia_did");
|
|
235
|
+
changed = true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (changed) {
|
|
239
|
+
const cleanUrl = url.pathname + (url.search || "") + (url.hash || "");
|
|
240
|
+
try {
|
|
241
|
+
window.history.replaceState(null, "", cleanUrl);
|
|
242
|
+
} catch {
|
|
243
|
+
// ignore
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// AT Protocol Functions
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Fetch the publication AT URI from the host site's well-known endpoint.
|
|
254
|
+
* @param {string} [origin] - Origin to fetch from (defaults to current page origin)
|
|
255
|
+
* @returns {Promise<string>} Publication AT URI
|
|
256
|
+
*/
|
|
257
|
+
async function fetchPublicationUri(origin) {
|
|
258
|
+
const base = origin ?? window.location.origin;
|
|
259
|
+
const url = `${base}/.well-known/site.standard.publication`;
|
|
260
|
+
const response = await fetch(url);
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`Could not fetch publication URI: ${response.status}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Accept either plain text (the AT URI itself) or JSON with a `uri` field.
|
|
266
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
267
|
+
if (contentType.includes("application/json")) {
|
|
268
|
+
const data = await response.json();
|
|
269
|
+
const uri = data?.uri ?? data?.atUri ?? data?.publication;
|
|
270
|
+
if (!uri) {
|
|
271
|
+
throw new Error("Publication response did not contain a URI");
|
|
272
|
+
}
|
|
273
|
+
return uri;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const text = (await response.text()).trim();
|
|
277
|
+
if (!text.startsWith("at://")) {
|
|
278
|
+
throw new Error(`Unexpected publication URI format: ${text}`);
|
|
279
|
+
}
|
|
280
|
+
return text;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Web Component
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
// SSR-safe base class - use HTMLElement in browser, empty class in Node.js
|
|
288
|
+
const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {};
|
|
289
|
+
|
|
290
|
+
class SequoiaSubscribe extends BaseElement {
|
|
291
|
+
constructor() {
|
|
292
|
+
super();
|
|
293
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
294
|
+
|
|
295
|
+
const styleTag = document.createElement("style");
|
|
296
|
+
styleTag.innerText = styles;
|
|
297
|
+
shadow.appendChild(styleTag);
|
|
298
|
+
|
|
299
|
+
const wrapper = document.createElement("div");
|
|
300
|
+
shadow.appendChild(wrapper);
|
|
301
|
+
wrapper.part = "container";
|
|
302
|
+
|
|
303
|
+
this.wrapper = wrapper;
|
|
304
|
+
this.subscribed = false;
|
|
305
|
+
this.state = { type: "idle" };
|
|
306
|
+
this.abortController = null;
|
|
307
|
+
this.render();
|
|
308
|
+
}
|
|
309
|
+
|
|
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
|
+
disconnectedCallback() {
|
|
327
|
+
this.abortController?.abort();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
attributeChangedCallback() {
|
|
331
|
+
if (this.state.type === "error" || this.state.type === "no-publication") {
|
|
332
|
+
this.state = { type: "idle" };
|
|
333
|
+
}
|
|
334
|
+
this.render();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get publicationUri() {
|
|
338
|
+
return this.getAttribute("publication-uri") ?? null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
get callbackUri() {
|
|
342
|
+
return this.getAttribute("callback-uri") ?? "https://sequoia.pub/subscribe";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get label() {
|
|
346
|
+
return this.getAttribute("label") ?? null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
get unsubscribeLabel() {
|
|
350
|
+
return this.getAttribute("unsubscribe-label") ?? null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
get buttonType() {
|
|
354
|
+
const val = this.getAttribute("button-type");
|
|
355
|
+
return val && val in BUTTON_TYPES ? val : "sequoia";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get hide() {
|
|
359
|
+
const hideAttr = this.getAttribute("hide");
|
|
360
|
+
return hideAttr === "auto";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async checkPublication() {
|
|
364
|
+
this.abortController?.abort();
|
|
365
|
+
this.abortController = new AbortController();
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const uri = this.publicationUri ?? (await fetchPublicationUri());
|
|
369
|
+
this.checkSubscription(uri);
|
|
370
|
+
} catch {
|
|
371
|
+
this.state = { type: "no-publication" };
|
|
372
|
+
this.render();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async checkSubscription(publicationUri) {
|
|
377
|
+
try {
|
|
378
|
+
const checkUrl = new URL(`${this.callbackUri}/check`);
|
|
379
|
+
checkUrl.searchParams.set("publicationUri", publicationUri);
|
|
380
|
+
|
|
381
|
+
// Pass the stored DID so the server can check without a session cookie
|
|
382
|
+
const storedDid = getStoredSubscriberDid();
|
|
383
|
+
if (storedDid) {
|
|
384
|
+
checkUrl.searchParams.set("did", storedDid);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const res = await fetch(checkUrl.toString(), {
|
|
388
|
+
credentials: "include",
|
|
389
|
+
});
|
|
390
|
+
if (!res.ok) return;
|
|
391
|
+
const data = await res.json();
|
|
392
|
+
if (data.subscribed) {
|
|
393
|
+
this.subscribed = true;
|
|
394
|
+
this.render();
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// Ignore errors — show default subscribe button
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async handleClick() {
|
|
402
|
+
if (this.state.type === "loading") {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Unsubscribe: redirect to full-page unsubscribe flow
|
|
407
|
+
if (this.subscribed) {
|
|
408
|
+
const publicationUri =
|
|
409
|
+
this.publicationUri ?? (await fetchPublicationUri());
|
|
410
|
+
window.location.href = `${this.callbackUri}?publicationUri=${encodeURIComponent(publicationUri)}&action=unsubscribe`;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.state = { type: "loading" };
|
|
415
|
+
this.render();
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const publicationUri =
|
|
419
|
+
this.publicationUri ?? (await fetchPublicationUri());
|
|
420
|
+
|
|
421
|
+
const response = await fetch(this.callbackUri, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "Content-Type": "application/json" },
|
|
424
|
+
credentials: "include",
|
|
425
|
+
referrerPolicy: "no-referrer-when-downgrade",
|
|
426
|
+
body: JSON.stringify({ publicationUri }),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const data = await response.json();
|
|
430
|
+
|
|
431
|
+
if (response.status === 401 && data.authenticated === false) {
|
|
432
|
+
// Redirect to the hosted subscribe page to complete OAuth,
|
|
433
|
+
// passing the current page URL (without credentials) as returnTo.
|
|
434
|
+
const subscribeUrl = new URL(data.subscribeUrl);
|
|
435
|
+
const pageUrl = new URL(window.location.href);
|
|
436
|
+
pageUrl.username = "";
|
|
437
|
+
pageUrl.password = "";
|
|
438
|
+
subscribeUrl.searchParams.set("returnTo", pageUrl.toString());
|
|
439
|
+
window.location.href = subscribeUrl.toString();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!response.ok) {
|
|
444
|
+
throw new Error(data.error ?? `HTTP ${response.status}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const { recordUri } = data;
|
|
448
|
+
|
|
449
|
+
// Store the DID from the record URI (at://did:aaa:bbb/...)
|
|
450
|
+
if (recordUri) {
|
|
451
|
+
const didMatch = recordUri.match(/^at:\/\/(did:[^/]+)/);
|
|
452
|
+
if (didMatch) {
|
|
453
|
+
storeSubscriberDid(didMatch[1]);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
this.subscribed = true;
|
|
458
|
+
this.state = { type: "idle" };
|
|
459
|
+
this.render();
|
|
460
|
+
|
|
461
|
+
this.dispatchEvent(
|
|
462
|
+
new CustomEvent("sequoia-subscribed", {
|
|
463
|
+
bubbles: true,
|
|
464
|
+
composed: true,
|
|
465
|
+
detail: { publicationUri, recordUri },
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (this.state.type !== "loading") return;
|
|
470
|
+
|
|
471
|
+
const message =
|
|
472
|
+
error instanceof Error ? error.message : "Failed to subscribe";
|
|
473
|
+
this.state = { type: "error", message };
|
|
474
|
+
this.render();
|
|
475
|
+
|
|
476
|
+
this.dispatchEvent(
|
|
477
|
+
new CustomEvent("sequoia-subscribe-error", {
|
|
478
|
+
bubbles: true,
|
|
479
|
+
composed: true,
|
|
480
|
+
detail: { message },
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
render() {
|
|
487
|
+
const { type } = this.state;
|
|
488
|
+
|
|
489
|
+
if (type === "no-publication") {
|
|
490
|
+
if (this.hide) {
|
|
491
|
+
this.wrapper.innerHTML = "";
|
|
492
|
+
this.wrapper.style.display = "none";
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const isLoading = type === "loading";
|
|
498
|
+
const config = BUTTON_TYPES[this.buttonType] ?? BUTTON_TYPES.sequoia;
|
|
499
|
+
|
|
500
|
+
const icon = isLoading
|
|
501
|
+
? `<span class="sequoia-loading-spinner"></span>`
|
|
502
|
+
: config.icon;
|
|
503
|
+
|
|
504
|
+
const label = this.subscribed
|
|
505
|
+
? (this.unsubscribeLabel ?? config.unsubscribe)
|
|
506
|
+
: (this.label ?? config.subscribe);
|
|
507
|
+
|
|
508
|
+
const errorHtml =
|
|
509
|
+
type === "error"
|
|
510
|
+
? `<span class="sequoia-error-message">${escapeHtml(this.state.message)}</span>`
|
|
511
|
+
: "";
|
|
512
|
+
|
|
513
|
+
this.wrapper.innerHTML = `
|
|
514
|
+
<button
|
|
515
|
+
class="sequoia-subscribe-button"
|
|
516
|
+
type="button"
|
|
517
|
+
part="button"
|
|
518
|
+
${isLoading ? "disabled" : ""}
|
|
519
|
+
aria-label="${label}"
|
|
520
|
+
>
|
|
521
|
+
${icon}
|
|
522
|
+
${label}
|
|
523
|
+
</button>
|
|
524
|
+
${errorHtml}
|
|
525
|
+
`;
|
|
526
|
+
|
|
527
|
+
const btn = this.wrapper.querySelector("button");
|
|
528
|
+
btn?.addEventListener("click", () => this.handleClick());
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Escape HTML special characters (no DOM dependency for SSR).
|
|
534
|
+
* @param {string} text
|
|
535
|
+
* @returns {string}
|
|
536
|
+
*/
|
|
537
|
+
function escapeHtml(text) {
|
|
538
|
+
return text
|
|
539
|
+
.replace(/&/g, "&")
|
|
540
|
+
.replace(/</g, "<")
|
|
541
|
+
.replace(/>/g, ">")
|
|
542
|
+
.replace(/"/g, """);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Register the custom element
|
|
546
|
+
if (typeof customElements !== "undefined") {
|
|
547
|
+
customElements.define("sequoia-subscribe", SequoiaSubscribe);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Export for module usage
|
|
551
|
+
export { SequoiaSubscribe };
|