sequoia-cli 0.4.0 → 0.5.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/dist/components/sequoia-comments.js +55 -55
- package/dist/components/sequoia-subscribe.js +379 -0
- package/dist/index.js +4270 -657
- package/package.json +6 -2
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Attributes:
|
|
15
15
|
* - document-uri: AT Protocol URI for the document (optional if link tag exists)
|
|
16
16
|
* - depth: Maximum depth of nested replies to fetch (default: 6)
|
|
17
|
+
* - hide: Set to "auto" to hide if no document link is detected
|
|
17
18
|
*
|
|
18
19
|
* CSS Custom Properties:
|
|
19
20
|
* - --sequoia-fg-color: Text color (default: #1f2937)
|
|
@@ -573,13 +574,24 @@ const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {};
|
|
|
573
574
|
class SequoiaComments extends BaseElement {
|
|
574
575
|
constructor() {
|
|
575
576
|
super();
|
|
576
|
-
|
|
577
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
578
|
+
|
|
579
|
+
const styleTag = document.createElement("style");
|
|
580
|
+
shadow.appendChild(styleTag);
|
|
581
|
+
styleTag.innerText = styles;
|
|
582
|
+
|
|
583
|
+
const container = document.createElement("div");
|
|
584
|
+
shadow.appendChild(container);
|
|
585
|
+
container.className = "sequoia-comments-container";
|
|
586
|
+
container.part = "container";
|
|
587
|
+
|
|
588
|
+
this.commentsContainer = container;
|
|
577
589
|
this.state = { type: "loading" };
|
|
578
590
|
this.abortController = null;
|
|
579
591
|
}
|
|
580
592
|
|
|
581
593
|
static get observedAttributes() {
|
|
582
|
-
return ["document-uri", "depth"];
|
|
594
|
+
return ["document-uri", "depth", "hide"];
|
|
583
595
|
}
|
|
584
596
|
|
|
585
597
|
connectedCallback() {
|
|
@@ -616,6 +628,11 @@ class SequoiaComments extends BaseElement {
|
|
|
616
628
|
return depthAttr ? parseInt(depthAttr, 10) : 6;
|
|
617
629
|
}
|
|
618
630
|
|
|
631
|
+
get hide() {
|
|
632
|
+
const hideAttr = this.getAttribute("hide");
|
|
633
|
+
return hideAttr === "auto";
|
|
634
|
+
}
|
|
635
|
+
|
|
619
636
|
async loadComments() {
|
|
620
637
|
// Cancel any in-flight request
|
|
621
638
|
this.abortController?.abort();
|
|
@@ -666,68 +683,54 @@ class SequoiaComments extends BaseElement {
|
|
|
666
683
|
}
|
|
667
684
|
|
|
668
685
|
render() {
|
|
669
|
-
const styleTag = `<style>${styles}</style>`;
|
|
670
|
-
|
|
671
686
|
switch (this.state.type) {
|
|
672
687
|
case "loading":
|
|
673
|
-
this.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
<span class="sequoia-loading-spinner"></span>
|
|
678
|
-
Loading comments...
|
|
679
|
-
</div>
|
|
688
|
+
this.commentsContainer.innerHTML = `
|
|
689
|
+
<div class="sequoia-loading">
|
|
690
|
+
<span class="sequoia-loading-spinner"></span>
|
|
691
|
+
Loading comments...
|
|
680
692
|
</div>
|
|
681
693
|
`;
|
|
682
694
|
break;
|
|
683
695
|
|
|
684
696
|
case "no-document":
|
|
685
|
-
this.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
<div class="sequoia-warning">
|
|
689
|
-
No document found. Add a <code><link rel="site.standard.document" href="at://..."></code> tag to your page.
|
|
690
|
-
</div>
|
|
697
|
+
this.commentsContainer.innerHTML = `
|
|
698
|
+
<div class="sequoia-warning">
|
|
699
|
+
No document found. Add a <code><link rel="site.standard.document" href="at://..."></code> tag to your page.
|
|
691
700
|
</div>
|
|
692
701
|
`;
|
|
702
|
+
if (this.hide) {
|
|
703
|
+
this.commentsContainer.style.display = "none";
|
|
704
|
+
}
|
|
693
705
|
break;
|
|
694
706
|
|
|
695
707
|
case "no-comments-enabled":
|
|
696
|
-
this.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
<div class="sequoia-empty">
|
|
700
|
-
Comments are not enabled for this post.
|
|
701
|
-
</div>
|
|
708
|
+
this.commentsContainer.innerHTML = `
|
|
709
|
+
<div class="sequoia-empty">
|
|
710
|
+
Comments are not enabled for this post.
|
|
702
711
|
</div>
|
|
703
712
|
`;
|
|
704
713
|
break;
|
|
705
714
|
|
|
706
715
|
case "empty":
|
|
707
|
-
this.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
<
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
<div class="sequoia-empty">
|
|
718
|
-
No comments yet. Be the first to reply on Bluesky!
|
|
719
|
-
</div>
|
|
716
|
+
this.commentsContainer.innerHTML = `
|
|
717
|
+
<div class="sequoia-comments-header">
|
|
718
|
+
<h3 class="sequoia-comments-title">Comments</h3>
|
|
719
|
+
<a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button">
|
|
720
|
+
${BLUESKY_ICON}
|
|
721
|
+
Reply on Bluesky
|
|
722
|
+
</a>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="sequoia-empty">
|
|
725
|
+
No comments yet. Be the first to reply on Bluesky!
|
|
720
726
|
</div>
|
|
721
727
|
`;
|
|
722
728
|
break;
|
|
723
729
|
|
|
724
730
|
case "error":
|
|
725
|
-
this.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
<div class="sequoia-error">
|
|
729
|
-
Failed to load comments: ${escapeHtml(this.state.message)}
|
|
730
|
-
</div>
|
|
731
|
+
this.commentsContainer.innerHTML = `
|
|
732
|
+
<div class="sequoia-error">
|
|
733
|
+
Failed to load comments: ${escapeHtml(this.state.message)}
|
|
731
734
|
</div>
|
|
732
735
|
`;
|
|
733
736
|
break;
|
|
@@ -740,19 +743,16 @@ class SequoiaComments extends BaseElement {
|
|
|
740
743
|
.join("");
|
|
741
744
|
const commentCount = this.countComments(replies);
|
|
742
745
|
|
|
743
|
-
this.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
<div class="sequoia-comments-list">
|
|
754
|
-
${threadsHtml}
|
|
755
|
-
</div>
|
|
746
|
+
this.commentsContainer.innerHTML = `
|
|
747
|
+
<div class="sequoia-comments-header">
|
|
748
|
+
<h3 class="sequoia-comments-title">${commentCount} Comment${commentCount !== 1 ? "s" : ""}</h3>
|
|
749
|
+
<a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button">
|
|
750
|
+
${BLUESKY_ICON}
|
|
751
|
+
Reply on Bluesky
|
|
752
|
+
</a>
|
|
753
|
+
</div>
|
|
754
|
+
<div class="sequoia-comments-list">
|
|
755
|
+
${threadsHtml}
|
|
756
756
|
</div>
|
|
757
757
|
`;
|
|
758
758
|
break;
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequoia Subscribe - A Bluesky-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
|
+
* - label: Button label text (default: "Subscribe on Bluesky")
|
|
17
|
+
* - hide: Set to "auto" to hide if no publication URI is detected
|
|
18
|
+
*
|
|
19
|
+
* CSS Custom Properties:
|
|
20
|
+
* - --sequoia-fg-color: Text color (default: #1f2937)
|
|
21
|
+
* - --sequoia-bg-color: Background color (default: #ffffff)
|
|
22
|
+
* - --sequoia-border-color: Border color (default: #e5e7eb)
|
|
23
|
+
* - --sequoia-accent-color: Accent/button color (default: #2563eb)
|
|
24
|
+
* - --sequoia-secondary-color: Secondary text color (default: #6b7280)
|
|
25
|
+
* - --sequoia-border-radius: Border radius (default: 8px)
|
|
26
|
+
*
|
|
27
|
+
* Events:
|
|
28
|
+
* - sequoia-subscribed: Fired when the subscription is created successfully.
|
|
29
|
+
* detail: { publicationUri: string, recordUri: string }
|
|
30
|
+
* - sequoia-subscribe-error: Fired when the subscription fails.
|
|
31
|
+
* detail: { message: string }
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Styles
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
const styles = `
|
|
39
|
+
:host {
|
|
40
|
+
display: inline-block;
|
|
41
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
42
|
+
color: var(--sequoia-fg-color, #1f2937);
|
|
43
|
+
line-height: 1.5;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.sequoia-subscribe-button {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 0.375rem;
|
|
54
|
+
padding: 0.5rem 1rem;
|
|
55
|
+
background: var(--sequoia-accent-color, #2563eb);
|
|
56
|
+
color: #ffffff;
|
|
57
|
+
border: none;
|
|
58
|
+
border-radius: var(--sequoia-border-radius, 8px);
|
|
59
|
+
font-size: 0.875rem;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
transition: background-color 0.15s ease;
|
|
64
|
+
font-family: inherit;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sequoia-subscribe-button:hover:not(:disabled) {
|
|
68
|
+
background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sequoia-subscribe-button:disabled {
|
|
72
|
+
opacity: 0.6;
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.sequoia-subscribe-button svg {
|
|
77
|
+
width: 1rem;
|
|
78
|
+
height: 1rem;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.sequoia-subscribe-button--success {
|
|
83
|
+
background: #16a34a;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sequoia-subscribe-button--success:hover:not(:disabled) {
|
|
87
|
+
background: color-mix(in srgb, #16a34a 85%, black);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sequoia-loading-spinner {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
width: 1rem;
|
|
93
|
+
height: 1rem;
|
|
94
|
+
border: 2px solid rgba(255, 255, 255, 0.4);
|
|
95
|
+
border-top-color: #ffffff;
|
|
96
|
+
border-radius: 50%;
|
|
97
|
+
animation: sequoia-spin 0.8s linear infinite;
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@keyframes sequoia-spin {
|
|
102
|
+
to { transform: rotate(360deg); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.sequoia-error-message {
|
|
106
|
+
display: inline-block;
|
|
107
|
+
font-size: 0.8125rem;
|
|
108
|
+
color: #dc2626;
|
|
109
|
+
margin-top: 0.375rem;
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Icons
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
const BLUESKY_ICON = `<svg class="sequoia-bsky-logo" viewBox="0 0 600 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
118
|
+
<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"/>
|
|
119
|
+
</svg>`;
|
|
120
|
+
|
|
121
|
+
const CHECK_ICON = `<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
122
|
+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
|
123
|
+
</svg>`;
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// AT Protocol Functions
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fetch the publication AT URI from the host site's well-known endpoint.
|
|
131
|
+
* @param {string} [origin] - Origin to fetch from (defaults to current page origin)
|
|
132
|
+
* @returns {Promise<string>} Publication AT URI
|
|
133
|
+
*/
|
|
134
|
+
async function fetchPublicationUri(origin) {
|
|
135
|
+
const base = origin ?? window.location.origin;
|
|
136
|
+
const url = `${base}/.well-known/site.standard.publication`;
|
|
137
|
+
const response = await fetch(url);
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Could not fetch publication URI: ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Accept either plain text (the AT URI itself) or JSON with a `uri` field.
|
|
143
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
144
|
+
if (contentType.includes("application/json")) {
|
|
145
|
+
const data = await response.json();
|
|
146
|
+
const uri = data?.uri ?? data?.atUri ?? data?.publication;
|
|
147
|
+
if (!uri) {
|
|
148
|
+
throw new Error("Publication response did not contain a URI");
|
|
149
|
+
}
|
|
150
|
+
return uri;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const text = (await response.text()).trim();
|
|
154
|
+
if (!text.startsWith("at://")) {
|
|
155
|
+
throw new Error(`Unexpected publication URI format: ${text}`);
|
|
156
|
+
}
|
|
157
|
+
return text;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Web Component
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
// SSR-safe base class - use HTMLElement in browser, empty class in Node.js
|
|
165
|
+
const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {};
|
|
166
|
+
|
|
167
|
+
class SequoiaSubscribe extends BaseElement {
|
|
168
|
+
constructor() {
|
|
169
|
+
super();
|
|
170
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
171
|
+
|
|
172
|
+
const styleTag = document.createElement("style");
|
|
173
|
+
styleTag.innerText = styles;
|
|
174
|
+
shadow.appendChild(styleTag);
|
|
175
|
+
|
|
176
|
+
const wrapper = document.createElement("div");
|
|
177
|
+
shadow.appendChild(wrapper);
|
|
178
|
+
wrapper.part = "container";
|
|
179
|
+
|
|
180
|
+
this.wrapper = wrapper;
|
|
181
|
+
this.state = { type: "idle" };
|
|
182
|
+
this.abortController = null;
|
|
183
|
+
this.render();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static get observedAttributes() {
|
|
187
|
+
return ["publication-uri", "callback-uri", "label", "hide"];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
connectedCallback() {
|
|
191
|
+
// Pre-check publication availability so hide="auto" can take effect
|
|
192
|
+
if (!this.publicationUri) {
|
|
193
|
+
this.checkPublication();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
disconnectedCallback() {
|
|
198
|
+
this.abortController?.abort();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
attributeChangedCallback() {
|
|
202
|
+
// Reset to idle if attributes change after an error or success
|
|
203
|
+
if (
|
|
204
|
+
this.state.type === "error" ||
|
|
205
|
+
this.state.type === "subscribed" ||
|
|
206
|
+
this.state.type === "no-publication"
|
|
207
|
+
) {
|
|
208
|
+
this.state = { type: "idle" };
|
|
209
|
+
}
|
|
210
|
+
this.render();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get publicationUri() {
|
|
214
|
+
return this.getAttribute("publication-uri") ?? null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get callbackUri() {
|
|
218
|
+
return this.getAttribute("callback-uri") ?? "https://sequoia.pub/subscribe";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get label() {
|
|
222
|
+
return this.getAttribute("label") ?? "Subscribe on Bluesky";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get hide() {
|
|
226
|
+
const hideAttr = this.getAttribute("hide");
|
|
227
|
+
return hideAttr === "auto";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async checkPublication() {
|
|
231
|
+
this.abortController?.abort();
|
|
232
|
+
this.abortController = new AbortController();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await fetchPublicationUri();
|
|
236
|
+
} catch {
|
|
237
|
+
this.state = { type: "no-publication" };
|
|
238
|
+
this.render();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async handleClick() {
|
|
243
|
+
if (this.state.type === "loading" || this.state.type === "subscribed") {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.state = { type: "loading" };
|
|
248
|
+
this.render();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const publicationUri =
|
|
252
|
+
this.publicationUri ?? (await fetchPublicationUri());
|
|
253
|
+
|
|
254
|
+
// POST to the callbackUri (e.g. https://sequoia.pub/subscribe).
|
|
255
|
+
// If the server reports the user isn't authenticated it returns a
|
|
256
|
+
// subscribeUrl for the full-page OAuth + subscription flow.
|
|
257
|
+
const response = await fetch(this.callbackUri, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
credentials: "include",
|
|
261
|
+
body: JSON.stringify({ publicationUri }),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
|
|
266
|
+
if (response.status === 401 && data.authenticated === false) {
|
|
267
|
+
// Redirect to the hosted subscribe page to complete OAuth
|
|
268
|
+
window.location.href = data.subscribeUrl;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
throw new Error(data.error ?? `HTTP ${response.status}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const { recordUri } = data;
|
|
277
|
+
this.state = { type: "subscribed", recordUri, publicationUri };
|
|
278
|
+
this.render();
|
|
279
|
+
|
|
280
|
+
this.dispatchEvent(
|
|
281
|
+
new CustomEvent("sequoia-subscribed", {
|
|
282
|
+
bubbles: true,
|
|
283
|
+
composed: true,
|
|
284
|
+
detail: { publicationUri, recordUri },
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// Don't overwrite state if we already navigated away
|
|
289
|
+
if (this.state.type !== "loading") return;
|
|
290
|
+
|
|
291
|
+
const message =
|
|
292
|
+
error instanceof Error ? error.message : "Failed to subscribe";
|
|
293
|
+
this.state = { type: "error", message };
|
|
294
|
+
this.render();
|
|
295
|
+
|
|
296
|
+
this.dispatchEvent(
|
|
297
|
+
new CustomEvent("sequoia-subscribe-error", {
|
|
298
|
+
bubbles: true,
|
|
299
|
+
composed: true,
|
|
300
|
+
detail: { message },
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
render() {
|
|
307
|
+
const { type } = this.state;
|
|
308
|
+
|
|
309
|
+
if (type === "no-publication") {
|
|
310
|
+
if (this.hide) {
|
|
311
|
+
this.wrapper.innerHTML = "";
|
|
312
|
+
this.wrapper.style.display = "none";
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const isLoading = type === "loading";
|
|
318
|
+
const isSubscribed = type === "subscribed";
|
|
319
|
+
|
|
320
|
+
const icon = isLoading
|
|
321
|
+
? `<span class="sequoia-loading-spinner"></span>`
|
|
322
|
+
: isSubscribed
|
|
323
|
+
? CHECK_ICON
|
|
324
|
+
: BLUESKY_ICON;
|
|
325
|
+
|
|
326
|
+
const label = isSubscribed ? "Subscribed" : this.label;
|
|
327
|
+
const buttonClass = [
|
|
328
|
+
"sequoia-subscribe-button",
|
|
329
|
+
isSubscribed ? "sequoia-subscribe-button--success" : "",
|
|
330
|
+
]
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.join(" ");
|
|
333
|
+
|
|
334
|
+
const errorHtml =
|
|
335
|
+
type === "error"
|
|
336
|
+
? `<span class="sequoia-error-message">${escapeHtml(this.state.message)}</span>`
|
|
337
|
+
: "";
|
|
338
|
+
|
|
339
|
+
this.wrapper.innerHTML = `
|
|
340
|
+
<button
|
|
341
|
+
class="${buttonClass}"
|
|
342
|
+
type="button"
|
|
343
|
+
part="button"
|
|
344
|
+
${isLoading || isSubscribed ? "disabled" : ""}
|
|
345
|
+
aria-label="${isSubscribed ? "Subscribed" : this.label}"
|
|
346
|
+
>
|
|
347
|
+
${icon}
|
|
348
|
+
${label}
|
|
349
|
+
</button>
|
|
350
|
+
${errorHtml}
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
if (type !== "subscribed") {
|
|
354
|
+
const btn = this.wrapper.querySelector("button");
|
|
355
|
+
btn?.addEventListener("click", () => this.handleClick());
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Escape HTML special characters (no DOM dependency for SSR).
|
|
362
|
+
* @param {string} text
|
|
363
|
+
* @returns {string}
|
|
364
|
+
*/
|
|
365
|
+
function escapeHtml(text) {
|
|
366
|
+
return text
|
|
367
|
+
.replace(/&/g, "&")
|
|
368
|
+
.replace(/</g, "<")
|
|
369
|
+
.replace(/>/g, ">")
|
|
370
|
+
.replace(/"/g, """);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Register the custom element
|
|
374
|
+
if (typeof customElements !== "undefined") {
|
|
375
|
+
customElements.define("sequoia-subscribe", SequoiaSubscribe);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Export for module usage
|
|
379
|
+
export { SequoiaSubscribe };
|