sequoia-cli 0.5.0-alpha.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 +1 -2
- package/dist/components/sequoia-subscribe.js +379 -0
- package/dist/index.js +4223 -647
- package/package.json +6 -2
|
@@ -588,7 +588,6 @@ class SequoiaComments extends BaseElement {
|
|
|
588
588
|
this.commentsContainer = container;
|
|
589
589
|
this.state = { type: "loading" };
|
|
590
590
|
this.abortController = null;
|
|
591
|
-
|
|
592
591
|
}
|
|
593
592
|
|
|
594
593
|
static get observedAttributes() {
|
|
@@ -701,7 +700,7 @@ class SequoiaComments extends BaseElement {
|
|
|
701
700
|
</div>
|
|
702
701
|
`;
|
|
703
702
|
if (this.hide) {
|
|
704
|
-
this.commentsContainer.style.display =
|
|
703
|
+
this.commentsContainer.style.display = "none";
|
|
705
704
|
}
|
|
706
705
|
break;
|
|
707
706
|
|
|
@@ -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 };
|