astro-atmosphere-comments 0.5.0 → 0.6.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/CHANGELOG.md +19 -0
- package/README.md +17 -6
- package/components/AtmosphereComments.astro +199 -39
- package/components/AtmosphereReactions.astro +68 -56
- package/components/Composer.astro +5 -2
- package/components/SignInForm.astro +55 -8
- package/components/atmosphere.css +246 -2
- package/index.d.ts +11 -0
- package/index.mjs +11 -0
- package/lib/client/account.ts +124 -16
- package/lib/client/composer.ts +16 -0
- package/lib/client/subscribe.ts +106 -0
- package/lib/permissions.ts +32 -8
- package/lib/public-settings.ts +2 -0
- package/lib/quoted.ts +4 -0
- package/lib/settings.ts +2 -0
- package/lib/uris.ts +8 -0
- package/locales/en.json +29 -10
- package/package.json +1 -1
- package/routes/post.ts +1 -0
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
// second press unfollows. Only
|
|
19
19
|
// `allowedAuthors` can be followed through your site.
|
|
20
20
|
//
|
|
21
|
+
// With the integration's `standardSite` option, a button marked
|
|
22
|
+
// `data-atmosphere-subscribe` (and `hidden`, as it needs JavaScript) becomes
|
|
23
|
+
// Subscribe / ✓ Subscribed for the site's publication. Anyone not signed in is
|
|
24
|
+
// offered the sign-in form first, and subscribed once they're back. Its label
|
|
25
|
+
// goes in an element inside it marked `data-atmosphere-subscribe-label`;
|
|
26
|
+
// without one it keeps its own content (style it by `aria-pressed`).
|
|
27
|
+
// (AtmosphereComments has its own panel for this under each post.)
|
|
28
|
+
//
|
|
21
29
|
// Signed-in readers like and repost from your page. Anyone else who presses
|
|
22
30
|
// Like or Repost is offered the sign-in form in an overlay, and the like or
|
|
23
31
|
// repost is done once they're back. Only posts by the integration's
|
|
@@ -25,7 +33,8 @@
|
|
|
25
33
|
//
|
|
26
34
|
// Props: locale, messages (as for AtmosphereComments).
|
|
27
35
|
import settings from "../lib/settings";
|
|
28
|
-
import { messagesFor, pickLocale, translate } from "../lib/i18n";
|
|
36
|
+
import { messagesFor, pickLocale, translate, translateHtml } from "../lib/i18n";
|
|
37
|
+
import { escapeHtml } from "../lib/translate";
|
|
29
38
|
import SignInForm from "./SignInForm.astro";
|
|
30
39
|
import Composer from "./Composer.astro";
|
|
31
40
|
import "./atmosphere.css";
|
|
@@ -47,6 +56,14 @@ const { base } = settings;
|
|
|
47
56
|
<dialog class="atmo atmo-signin" aria-label={t("dialog.heading")} data-signin-dialog>
|
|
48
57
|
<button type="button" class="atmo-signin__close" aria-label={t("dialog.close")} data-close>×</button>
|
|
49
58
|
<SignInForm id="atmo-dialog" returnTo={Astro.url.pathname} messages={messages} lang={lang} heading={t("dialog.heading")} />
|
|
59
|
+
<p
|
|
60
|
+
class="atmo__elsewhere"
|
|
61
|
+
data-dialog-elsewhere
|
|
62
|
+
hidden
|
|
63
|
+
set:html={translateHtml(messages, lang, "elsewhere.text", {
|
|
64
|
+
link: `<a target="_blank" rel="noopener" class="atmo__elsewhere-link">${escapeHtml(t("elsewhere.link"))}</a>`,
|
|
65
|
+
})}
|
|
66
|
+
/>
|
|
50
67
|
<p class="atmo-signin__status" role="status" data-dialog-status></p>
|
|
51
68
|
</dialog>
|
|
52
69
|
{/* Before asking the reader's account host for one more permission: what it's for. */}
|
|
@@ -77,6 +94,7 @@ const { base } = settings;
|
|
|
77
94
|
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
78
95
|
import { MAX, quotePreview, setUpComposer, type Posted } from "../lib/client/composer";
|
|
79
96
|
import { following, handleSignInForm, me as whoIsSignedIn, send as act, signIn, signOut as endSignIn, state } from "../lib/client/account";
|
|
97
|
+
import { addSubscribeButtons, finishPendingSubscribe, loadSubscribed } from "../lib/client/subscribe";
|
|
80
98
|
import type { Permission } from "../lib/permissions";
|
|
81
99
|
|
|
82
100
|
const HEART = '<path d="M12 20.5s-7.5-4.4-7.5-10.1A4.4 4.4 0 0 1 12 7.6a4.4 4.4 0 0 1 7.5 2.8c0 5.7-7.5 10.1-7.5 10.1Z" />';
|
|
@@ -232,14 +250,28 @@ const { base } = settings;
|
|
|
232
250
|
return element;
|
|
233
251
|
};
|
|
234
252
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
253
|
+
// The sign-in overlay: says what it's for, and comes back to `hash`,
|
|
254
|
+
// where what the reader pressed is finished.
|
|
255
|
+
const dialogHeading = form.querySelector("label")!;
|
|
256
|
+
const dialogElsewhere = dialog.querySelector<HTMLElement>("[data-dialog-elsewhere]")!;
|
|
257
|
+
const openSignIn = (heading: string, hash: string, problem = "", blueskyUrl = "") => {
|
|
258
|
+
dialogHeading.textContent = heading;
|
|
259
|
+
dialog.setAttribute("aria-label", heading);
|
|
260
|
+
form.querySelector<HTMLInputElement>("[data-return-to]")!.value = `${location.pathname}${location.search}${hash}`;
|
|
239
261
|
status.textContent = problem;
|
|
262
|
+
dialogElsewhere.hidden = !blueskyUrl;
|
|
263
|
+
if (blueskyUrl) dialogElsewhere.querySelector("a")!.href = blueskyUrl;
|
|
240
264
|
if (!dialog.open) dialog.showModal();
|
|
241
265
|
form.querySelector<HTMLInputElement>('input[name="handle"]')!.focus();
|
|
242
266
|
};
|
|
267
|
+
const blueskyUrlOf = (uri: string) => posts().find((post) => post.dataset.atmospherePost === uri)?.dataset.atmosphereUrl ?? "";
|
|
268
|
+
const SIGN_IN_HEADINGS = { like: "signInFirst.likePost", repost: "signInFirst.repostPost", reply: "signInFirst.reply", quote: "signInFirst.quotePost" };
|
|
269
|
+
// Not signed in: the overlay, coming back to finish this like or repost.
|
|
270
|
+
const askToSignIn = (kind: Kind, uri: string, problem = "") =>
|
|
271
|
+
openSignIn(t(SIGN_IN_HEADINGS[kind]), `#atmo-react=${kind}:${encodeURIComponent(uri)}`, problem, blueskyUrlOf(uri));
|
|
272
|
+
// The same for Reply and Quote, coming back to the comment box.
|
|
273
|
+
const askToSignInToCompose = (mode: "reply" | "quote", uri: string, problem = "") =>
|
|
274
|
+
openSignIn(t(SIGN_IN_HEADINGS[mode]), `#atmo-compose=${mode}:${encodeURIComponent(uri)}`, problem, blueskyUrlOf(uri));
|
|
243
275
|
|
|
244
276
|
|
|
245
277
|
const icon = (paths: string) => `<svg viewBox="0 0 24 24" aria-hidden="true">${paths}</svg>`;
|
|
@@ -308,7 +340,8 @@ const { base } = settings;
|
|
|
308
340
|
};
|
|
309
341
|
|
|
310
342
|
// Reply and Quote: the comment box in an overlay for signed-in readers,
|
|
311
|
-
// otherwise the
|
|
343
|
+
// otherwise the sign-in overlay. ⌘-click or a middle click opens the post
|
|
344
|
+
// on Bluesky.
|
|
312
345
|
// Icons only, as on Bluesky, with their names for screen readers and as
|
|
313
346
|
// tooltips.
|
|
314
347
|
const bskyLink = (post: HTMLElement, paths: string) => {
|
|
@@ -321,8 +354,9 @@ const { base } = settings;
|
|
|
321
354
|
labelLink(link);
|
|
322
355
|
link.innerHTML = icon(paths);
|
|
323
356
|
link.addEventListener("click", (event) => {
|
|
324
|
-
if (
|
|
357
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
325
358
|
event.preventDefault();
|
|
359
|
+
if (!signedIn) return askToSignInToCompose(mode, post.dataset.atmospherePost!);
|
|
326
360
|
// A sign-in without posting (from an older version): asked for first.
|
|
327
361
|
if (!can("post")) return void askPermission("post", `#atmo-compose=${mode}:${encodeURIComponent(post.dataset.atmospherePost!)}`);
|
|
328
362
|
openCompose(post, mode);
|
|
@@ -439,6 +473,14 @@ const { base } = settings;
|
|
|
439
473
|
}
|
|
440
474
|
};
|
|
441
475
|
|
|
476
|
+
// Subscribe buttons the site places (the comments look after their own).
|
|
477
|
+
const askToSubscribe = (problem = "") => openSignIn(t("subscribe.signIn"), "#atmo-subscribe", problem);
|
|
478
|
+
addSubscribeButtons(
|
|
479
|
+
[...document.querySelectorAll<HTMLButtonElement>("[data-atmosphere-subscribe]")].filter((button) => !button.closest("atmosphere-comments")),
|
|
480
|
+
t,
|
|
481
|
+
askToSubscribe,
|
|
482
|
+
);
|
|
483
|
+
|
|
442
484
|
// Back from approving following: follow as they asked.
|
|
443
485
|
const finishPendingFollow = async () => {
|
|
444
486
|
const pending = location.hash.match(/^#atmo-follow=(.+)$/);
|
|
@@ -477,16 +519,28 @@ const { base } = settings;
|
|
|
477
519
|
if (button && button.getAttribute("aria-pressed") !== "true") await react(post, button, kind as Kind, true);
|
|
478
520
|
};
|
|
479
521
|
|
|
480
|
-
// Back from allowing posting: open the Reply or Quote box they pressed.
|
|
481
|
-
const finishPendingCompose = () => {
|
|
522
|
+
// Back from signing in or allowing posting: open the Reply or Quote box they pressed.
|
|
523
|
+
const finishPendingCompose = async () => {
|
|
482
524
|
const pending = location.hash.match(/^#atmo-compose=(reply|quote):(.+)$/);
|
|
483
525
|
if (!pending) return;
|
|
484
526
|
const url = new URL(location.href);
|
|
527
|
+
const problem = url.searchParams.get("atmosphere") ?? "";
|
|
485
528
|
if (!document.querySelector("atmosphere-comments")) url.searchParams.delete("atmosphere");
|
|
486
|
-
history.replaceState(history.state, "", `${url.pathname}${url.search}`);
|
|
487
529
|
const uri = decodeURIComponent(pending[2]);
|
|
530
|
+
const mode = pending[1] as "reply" | "quote";
|
|
531
|
+
// The page may still be loading its posts (a server island, say).
|
|
532
|
+
for (let tries = 0; tries < 40 && !posts().some((post) => post.dataset.atmospherePost === uri); tries++) {
|
|
533
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
534
|
+
}
|
|
535
|
+
history.replaceState(history.state, "", `${url.pathname}${url.search}`);
|
|
488
536
|
const post = posts().find((candidate) => candidate.dataset.atmospherePost === uri);
|
|
489
|
-
if (post
|
|
537
|
+
if (!post) return;
|
|
538
|
+
post.scrollIntoView({ block: "center" });
|
|
539
|
+
if (!signedIn) {
|
|
540
|
+
if (SIGN_IN_PROBLEMS[problem]) askToSignInToCompose(mode, uri, t(SIGN_IN_PROBLEMS[problem]));
|
|
541
|
+
} else if (can("post")) {
|
|
542
|
+
openCompose(post, mode);
|
|
543
|
+
}
|
|
490
544
|
};
|
|
491
545
|
|
|
492
546
|
// The reader's avatar wherever the page marks data-atmosphere-account,
|
|
@@ -566,11 +620,13 @@ const { base } = settings;
|
|
|
566
620
|
addActions();
|
|
567
621
|
await loadState();
|
|
568
622
|
await finishPending();
|
|
569
|
-
finishPendingCompose();
|
|
623
|
+
await finishPendingCompose();
|
|
570
624
|
if (signedIn) {
|
|
571
625
|
await setUpFollow();
|
|
572
626
|
await finishPendingFollow();
|
|
627
|
+
await loadSubscribed();
|
|
573
628
|
}
|
|
629
|
+
await finishPendingSubscribe(t, askToSubscribe);
|
|
574
630
|
});
|
|
575
631
|
|
|
576
632
|
// Posts that arrive later (a server island, say) get buttons too.
|
|
@@ -587,18 +643,6 @@ const { base } = settings;
|
|
|
587
643
|
</script>
|
|
588
644
|
|
|
589
645
|
<style>
|
|
590
|
-
.atmo-signin {
|
|
591
|
-
/* Centred on the screen, whatever margin a site gives .atmo. */
|
|
592
|
-
margin: auto;
|
|
593
|
-
width: min(92vw, 520px);
|
|
594
|
-
padding: 22px 22px 18px;
|
|
595
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
596
|
-
border-radius: calc(var(--atmo-radius, 10px) + 6px);
|
|
597
|
-
background: var(--atmo-dialog-bg, var(--atmo-menu-bg, Canvas));
|
|
598
|
-
color: var(--atmo-text, CanvasText);
|
|
599
|
-
box-shadow: 0 24px 60px rgb(0 0 0 / 0.35);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
646
|
.atmo-permission p {
|
|
603
647
|
margin: 0 0 10px;
|
|
604
648
|
}
|
|
@@ -609,28 +653,6 @@ const { base } = settings;
|
|
|
609
653
|
font-weight: 700;
|
|
610
654
|
}
|
|
611
655
|
|
|
612
|
-
.atmo-signin::backdrop {
|
|
613
|
-
background: rgb(0 0 0 / 0.55);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
.atmo-signin :global(.atmo__login) {
|
|
617
|
-
margin-top: 0;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
.atmo-signin__close {
|
|
621
|
-
position: absolute;
|
|
622
|
-
top: 8px;
|
|
623
|
-
right: 10px;
|
|
624
|
-
padding: 4px 8px;
|
|
625
|
-
border: 0;
|
|
626
|
-
background: none;
|
|
627
|
-
color: var(--atmo-muted, inherit);
|
|
628
|
-
font: inherit;
|
|
629
|
-
font-size: 1.4em;
|
|
630
|
-
line-height: 1;
|
|
631
|
-
cursor: pointer;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
656
|
.atmo-compose {
|
|
635
657
|
width: min(92vw, 600px);
|
|
636
658
|
}
|
|
@@ -643,16 +665,6 @@ const { base } = settings;
|
|
|
643
665
|
margin: 0 0 12px;
|
|
644
666
|
}
|
|
645
667
|
|
|
646
|
-
.atmo-signin__status {
|
|
647
|
-
margin: 10px 0 0;
|
|
648
|
-
color: var(--atmo-muted, inherit);
|
|
649
|
-
font-size: 0.9em;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
.atmo-signin__status:empty {
|
|
653
|
-
display: none;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
668
|
/* The row under each post. Defaults carry no weight, so a site's own
|
|
657
669
|
rules always win. */
|
|
658
670
|
:global(:where(.atmo-post-actions)) {
|
|
@@ -56,7 +56,7 @@ const th = (key: string, html: Record<string, string>) => translateHtml(messages
|
|
|
56
56
|
<div class="atmo__quoted">
|
|
57
57
|
<p class="atmo__preview-author">
|
|
58
58
|
<span class="atmo__avatar atmo__avatar--small" data-quoted-avatar aria-hidden="true"></span>
|
|
59
|
-
<strong data-quoted-name></strong> <span data-quoted-handle></span>
|
|
59
|
+
<strong data-quoted-name></strong> <span data-quoted-handle></span> <span data-quoted-age></span>
|
|
60
60
|
</p>
|
|
61
61
|
<p class="atmo__preview-text" data-quoted-text></p>
|
|
62
62
|
<div class="atmo__card" data-card hidden>
|
|
@@ -64,7 +64,10 @@ const th = (key: string, html: Record<string, string>) => translateHtml(messages
|
|
|
64
64
|
<div>
|
|
65
65
|
<strong data-card-title></strong>
|
|
66
66
|
<span data-card-description></span>
|
|
67
|
-
<span class="atmo__card-domain"
|
|
67
|
+
<span class="atmo__card-domain">
|
|
68
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3c2.5 2.5 3.8 5.5 3.8 9s-1.3 6.5-3.8 9c-2.5-2.5-3.8-5.5-3.8-9s1.3-6.5 3.8-9Z" /></svg>
|
|
69
|
+
<span data-card-domain></span>
|
|
70
|
+
</span>
|
|
68
71
|
</div>
|
|
69
72
|
</div>
|
|
70
73
|
</div>
|
|
@@ -44,6 +44,7 @@ const link = (href: string, text: string) =>
|
|
|
44
44
|
set:html={th("signIn.anyAccount", {
|
|
45
45
|
atmosphereAccount: link("https://atmosphereaccount.com", t("signIn.atmosphereAccount")),
|
|
46
46
|
bluesky: link("https://bsky.app", "Bluesky"),
|
|
47
|
+
createAccount: link("https://atmosphereaccount.com", t("signIn.createAccount")),
|
|
47
48
|
})}
|
|
48
49
|
/>
|
|
49
50
|
<div class="atmo__row">
|
|
@@ -76,17 +77,19 @@ const link = (href: string, text: string) =>
|
|
|
76
77
|
{permissions.map((permission) => <input type="hidden" name="permission" value={permission} />)}
|
|
77
78
|
<button type="submit" class="atmo__button atmo__button--sign-in">
|
|
78
79
|
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
80
|
+
<span class="atmo__spinner" aria-hidden="true"></span>
|
|
79
81
|
{t("signIn.button")}
|
|
80
82
|
</button>
|
|
81
83
|
</div>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
{/* The detail, for anyone who wants it. */}
|
|
85
|
+
<details class="atmo__details">
|
|
86
|
+
<summary>{t("signIn.more")}</summary>
|
|
87
|
+
<p class="atmo__hint">{t("signIn.privacy")}</p>
|
|
88
|
+
<p class="atmo__hint">
|
|
89
|
+
{t("signIn.access")}
|
|
90
|
+
{policyUri && <Fragment> <a href={policyUri} target="_blank" rel="noopener">{t("signIn.policy")}</a></Fragment>}
|
|
91
|
+
</p>
|
|
92
|
+
</details>
|
|
90
93
|
</form>
|
|
91
94
|
|
|
92
95
|
<style>
|
|
@@ -105,6 +108,17 @@ const link = (href: string, text: string) =>
|
|
|
105
108
|
font-weight: 700;
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
.atmo__details summary {
|
|
112
|
+
width: fit-content;
|
|
113
|
+
color: var(--atmo-muted, inherit);
|
|
114
|
+
font-size: 0.85em;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.atmo__details .atmo__hint {
|
|
119
|
+
margin-top: 6px;
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
.atmo__hint {
|
|
109
123
|
margin: 0;
|
|
110
124
|
color: var(--atmo-muted, inherit);
|
|
@@ -165,6 +179,39 @@ const link = (href: string, text: string) =>
|
|
|
165
179
|
cursor: pointer;
|
|
166
180
|
}
|
|
167
181
|
|
|
182
|
+
/* While signing in, a spinner takes the logo's place, so the button keeps its size. */
|
|
183
|
+
.atmo__spinner {
|
|
184
|
+
display: none;
|
|
185
|
+
flex: none;
|
|
186
|
+
box-sizing: border-box;
|
|
187
|
+
width: 20px;
|
|
188
|
+
height: 20px;
|
|
189
|
+
border: 2px solid currentColor;
|
|
190
|
+
border-right-color: transparent;
|
|
191
|
+
border-radius: 50%;
|
|
192
|
+
animation: atmo-spin 700ms linear infinite;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
[data-signing-in] .atmo__spinner {
|
|
196
|
+
display: inline-block;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
[data-signing-in] .atmo__button .atmo__logo {
|
|
200
|
+
display: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@keyframes atmo-spin {
|
|
204
|
+
to {
|
|
205
|
+
transform: rotate(360deg);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@media (prefers-reduced-motion: reduce) {
|
|
210
|
+
.atmo__spinner {
|
|
211
|
+
animation-duration: 2.5s;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
168
215
|
.atmo__button .atmo__logo {
|
|
169
216
|
flex: none;
|
|
170
217
|
width: 20px;
|
|
@@ -118,6 +118,222 @@ dialog.atmo:not([open]) {
|
|
|
118
118
|
gap: 8px;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/* The sign-in overlay, shared by both components. */
|
|
122
|
+
dialog.atmo.atmo-signin {
|
|
123
|
+
/* Centred on the screen, whatever margin a site gives .atmo. */
|
|
124
|
+
margin: auto;
|
|
125
|
+
width: min(92vw, 520px);
|
|
126
|
+
padding: 22px 22px 18px;
|
|
127
|
+
border: 1px solid var(--atmo-border, #8886);
|
|
128
|
+
border-radius: calc(var(--atmo-radius, 10px) + 6px);
|
|
129
|
+
background: var(--atmo-dialog-bg, var(--atmo-menu-bg, Canvas));
|
|
130
|
+
color: var(--atmo-text, CanvasText);
|
|
131
|
+
box-shadow: 0 24px 60px rgb(0 0 0 / 0.35);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The sign-in form is short, and its account suggestions can reach past the
|
|
135
|
+
overlay's edge. (The comment box's overlay can be tall, so it keeps scrolling.) */
|
|
136
|
+
dialog.atmo.atmo-signin:has(> .atmo__login) {
|
|
137
|
+
overflow: visible;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* As wide as the form, so names and handles fit on one line. */
|
|
141
|
+
.atmo-signin .atmo__suggestions {
|
|
142
|
+
min-width: min(474px, calc(92vw - 46px));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.atmo-signin::backdrop {
|
|
146
|
+
background: rgb(0 0 0 / 0.55);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.atmo-signin .atmo__login {
|
|
150
|
+
margin-top: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.atmo-signin__close {
|
|
154
|
+
position: absolute;
|
|
155
|
+
top: 8px;
|
|
156
|
+
right: 10px;
|
|
157
|
+
padding: 4px 8px;
|
|
158
|
+
border: 0;
|
|
159
|
+
background: none;
|
|
160
|
+
color: var(--atmo-muted, inherit);
|
|
161
|
+
font: inherit;
|
|
162
|
+
font-size: 1.4em;
|
|
163
|
+
line-height: 1;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.atmo-signin__status {
|
|
168
|
+
margin: 10px 0 0;
|
|
169
|
+
color: var(--atmo-muted, inherit);
|
|
170
|
+
font-size: 0.9em;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.atmo-signin__status:empty {
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Signed out: looks like the comment box, and opens the sign-in overlay. */
|
|
178
|
+
.atmo__prompt-box {
|
|
179
|
+
display: grid;
|
|
180
|
+
gap: 8px;
|
|
181
|
+
max-width: 560px;
|
|
182
|
+
margin-top: 14px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.atmo__prompt-box[hidden] {
|
|
186
|
+
display: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.atmo__prompt-box .atmo__button:disabled {
|
|
190
|
+
cursor: not-allowed;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.atmo__prompt {
|
|
194
|
+
/* A button centres its text; a text box starts at the top left. */
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: flex-start;
|
|
197
|
+
width: 100%;
|
|
198
|
+
min-height: 84px;
|
|
199
|
+
padding: 12px 14px;
|
|
200
|
+
border: 1px solid var(--atmo-border, #8886);
|
|
201
|
+
border-radius: var(--atmo-radius, 10px);
|
|
202
|
+
background: var(--atmo-field-bg, transparent);
|
|
203
|
+
color: var(--atmo-muted, inherit);
|
|
204
|
+
font: inherit;
|
|
205
|
+
text-align: left;
|
|
206
|
+
vertical-align: top;
|
|
207
|
+
cursor: text;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.atmo__prompt:hover,
|
|
211
|
+
.atmo__prompt:focus-visible {
|
|
212
|
+
border-color: var(--atmo-accent, #1185fe);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Subscribe and its menu: a split button, with Like and Repost once signed
|
|
216
|
+
in, or on its own above the sign-in form. */
|
|
217
|
+
.atmo-subscribe {
|
|
218
|
+
position: relative;
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.atmo-subscribe[hidden] {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.atmo-subscribe .atmo-subscribe__button {
|
|
227
|
+
border-radius: 999px 0 0 999px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.atmo-subscribe__button[hidden] {
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.atmo-subscribe__button:disabled {
|
|
235
|
+
opacity: 0.6;
|
|
236
|
+
cursor: progress;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.atmo-subscribe__logo {
|
|
240
|
+
flex: none;
|
|
241
|
+
width: 18px;
|
|
242
|
+
height: 18px;
|
|
243
|
+
background-color: currentColor;
|
|
244
|
+
mask: var(--atmo-logo) center / contain no-repeat;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.atmo-subscribe .atmo-subscribe__more {
|
|
248
|
+
padding: 0 10px 0 8px;
|
|
249
|
+
border-left: 0;
|
|
250
|
+
border-radius: 0 999px 999px 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* Subscribed: both halves look like one button. */
|
|
254
|
+
.atmo-subscribe:has([aria-pressed="true"]) .atmo-subscribe__more {
|
|
255
|
+
border-color: color-mix(in srgb, var(--atmo-accent, #1185fe) 40%, transparent);
|
|
256
|
+
background: color-mix(in srgb, var(--atmo-accent, #1185fe) 8%, transparent);
|
|
257
|
+
color: var(--atmo-accent, #1185fe);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.atmo-subscribe .atmo-subscribe__more svg {
|
|
261
|
+
width: 15px;
|
|
262
|
+
height: 15px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.atmo-subscribe__menu {
|
|
266
|
+
position: absolute;
|
|
267
|
+
z-index: 20;
|
|
268
|
+
top: calc(100% + 6px);
|
|
269
|
+
left: 0;
|
|
270
|
+
display: grid;
|
|
271
|
+
width: min(260px, 80vw);
|
|
272
|
+
padding: 6px;
|
|
273
|
+
border: 1px solid var(--atmo-border, #8886);
|
|
274
|
+
border-radius: var(--atmo-radius, 10px);
|
|
275
|
+
background: var(--atmo-menu-bg, Canvas);
|
|
276
|
+
box-shadow: 0 12px 30px rgb(0 0 0 / 0.25);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.atmo-subscribe__menu[hidden] {
|
|
280
|
+
display: none;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.atmo-subscribe__note {
|
|
284
|
+
margin: 0 0 6px;
|
|
285
|
+
padding: 6px 10px 10px;
|
|
286
|
+
border-bottom: 1px solid var(--atmo-border, #8886);
|
|
287
|
+
color: var(--atmo-muted, inherit);
|
|
288
|
+
font-size: 0.85em;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.atmo-subscribe__menu > a[hidden] {
|
|
292
|
+
display: none;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.atmo-subscribe__menu svg {
|
|
296
|
+
flex: none;
|
|
297
|
+
width: 16px;
|
|
298
|
+
height: 16px;
|
|
299
|
+
fill: none;
|
|
300
|
+
stroke: currentColor;
|
|
301
|
+
stroke-width: 1.8;
|
|
302
|
+
stroke-linecap: round;
|
|
303
|
+
stroke-linejoin: round;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.atmo-subscribe__menu svg circle,
|
|
307
|
+
.atmo-subscribe__menu svg.atmo-subscribe__filled {
|
|
308
|
+
fill: currentColor;
|
|
309
|
+
stroke: none;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.atmo-subscribe__menu > a,
|
|
313
|
+
.atmo-subscribe__menu > button {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 9px;
|
|
317
|
+
padding: 7px 10px;
|
|
318
|
+
border: 0;
|
|
319
|
+
border-radius: calc(var(--atmo-radius, 10px) - 4px);
|
|
320
|
+
background: none;
|
|
321
|
+
color: var(--atmo-text, inherit);
|
|
322
|
+
font: inherit;
|
|
323
|
+
font-size: 0.9em;
|
|
324
|
+
text-align: left;
|
|
325
|
+
text-decoration: none;
|
|
326
|
+
cursor: pointer;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.atmo-subscribe__menu > a:hover,
|
|
330
|
+
.atmo-subscribe__menu > a:focus-visible,
|
|
331
|
+
.atmo-subscribe__menu > button:hover,
|
|
332
|
+
.atmo-subscribe__menu > button:focus-visible {
|
|
333
|
+
background: var(--atmo-field-bg, #8882);
|
|
334
|
+
color: var(--atmo-accent, #1185fe);
|
|
335
|
+
}
|
|
336
|
+
|
|
121
337
|
.atmo__action {
|
|
122
338
|
display: inline-flex;
|
|
123
339
|
align-items: center;
|
|
@@ -149,7 +365,9 @@ dialog.atmo:not([open]) {
|
|
|
149
365
|
}
|
|
150
366
|
|
|
151
367
|
.atmo__action[aria-pressed="true"] {
|
|
152
|
-
|
|
368
|
+
/* Softer than the text, like the outlines of the buttons beside it. */
|
|
369
|
+
border-color: color-mix(in srgb, var(--atmo-accent, #1185fe) 40%, transparent);
|
|
370
|
+
background: color-mix(in srgb, var(--atmo-accent, #1185fe) 8%, transparent);
|
|
153
371
|
color: var(--atmo-accent, #1185fe);
|
|
154
372
|
}
|
|
155
373
|
|
|
@@ -358,6 +576,10 @@ dialog.atmo:not([open]) {
|
|
|
358
576
|
overflow-wrap: anywhere;
|
|
359
577
|
}
|
|
360
578
|
|
|
579
|
+
.atmo__preview-text:empty {
|
|
580
|
+
display: none;
|
|
581
|
+
}
|
|
582
|
+
|
|
361
583
|
.atmo__preview-text.is-placeholder {
|
|
362
584
|
color: var(--atmo-muted, inherit);
|
|
363
585
|
font-style: italic;
|
|
@@ -386,6 +608,13 @@ dialog.atmo:not([open]) {
|
|
|
386
608
|
padding: 10px 12px;
|
|
387
609
|
}
|
|
388
610
|
|
|
611
|
+
.atmo__card [data-card-title] {
|
|
612
|
+
display: -webkit-box;
|
|
613
|
+
overflow: hidden;
|
|
614
|
+
-webkit-box-orient: vertical;
|
|
615
|
+
-webkit-line-clamp: 2;
|
|
616
|
+
}
|
|
617
|
+
|
|
389
618
|
.atmo__card [data-card-description] {
|
|
390
619
|
display: -webkit-box;
|
|
391
620
|
overflow: hidden;
|
|
@@ -393,10 +622,25 @@ dialog.atmo:not([open]) {
|
|
|
393
622
|
-webkit-line-clamp: 2;
|
|
394
623
|
}
|
|
395
624
|
|
|
396
|
-
.atmo__card-domain {
|
|
625
|
+
.atmo__card .atmo__card-domain {
|
|
626
|
+
display: flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
gap: 4px;
|
|
629
|
+
margin-top: 6px;
|
|
630
|
+
padding-top: 6px;
|
|
631
|
+
border-top: 1px solid var(--atmo-border, #8886);
|
|
397
632
|
font-size: 0.85em;
|
|
398
633
|
}
|
|
399
634
|
|
|
635
|
+
.atmo__card-domain svg {
|
|
636
|
+
flex: none;
|
|
637
|
+
width: 12px;
|
|
638
|
+
height: 12px;
|
|
639
|
+
fill: none;
|
|
640
|
+
stroke: currentColor;
|
|
641
|
+
stroke-width: 1.8;
|
|
642
|
+
}
|
|
643
|
+
|
|
400
644
|
/* The likes and reposts, and the comments. These are drawn by the script,
|
|
401
645
|
and wrapped in :where() so they carry no weight: any rule a site writes
|
|
402
646
|
for these classes wins. */
|
package/index.d.ts
CHANGED
|
@@ -16,6 +16,17 @@ export type AtmosphereCommentsOptions = {
|
|
|
16
16
|
/** Your privacy policy and terms, if you have them (absolute addresses). */
|
|
17
17
|
policyUri?: string;
|
|
18
18
|
tosUri?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Your Standard.site publication, if you publish one. Turns on the Subscribe
|
|
21
|
+
* button, and makes Like under a post also recommend its Standard.site
|
|
22
|
+
* record. Readers are asked for this in the same approval as everything else.
|
|
23
|
+
*/
|
|
24
|
+
standardSite?: {
|
|
25
|
+
/** The publication record's AT URI: at://did:…/site.standard.publication/… */
|
|
26
|
+
publication: string;
|
|
27
|
+
/** Your feed's address, for the menu beside Subscribe, e.g. "/feed/". */
|
|
28
|
+
feed?: string;
|
|
29
|
+
};
|
|
19
30
|
/**
|
|
20
31
|
* Comments never to show: bsky.app post or profile addresses, AT URIs,
|
|
21
32
|
* handles or DIDs (on top of replies hidden on Bluesky and moderation labels).
|
package/index.mjs
CHANGED
|
@@ -44,6 +44,7 @@ const ROUTES = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const DID = /^did:[a-z0-9]+:[a-zA-Z0-9._:%-]+$/;
|
|
47
|
+
const PUBLICATION = /^at:\/\/did:[a-z0-9]+:[a-zA-Z0-9._:%-]+\/site\.standard\.publication\/[a-zA-Z0-9._~-]+$/;
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* @param {import("./index").AtmosphereCommentsOptions} options
|
|
@@ -57,6 +58,10 @@ export default function atmosphereComments(options) {
|
|
|
57
58
|
"atmosphere-comments: `allowedAuthors` must list the DIDs (did:plc:… or did:web:…) of the accounts that post your announcements.",
|
|
58
59
|
);
|
|
59
60
|
}
|
|
61
|
+
const standardSite = options.standardSite;
|
|
62
|
+
if (standardSite && !PUBLICATION.test(standardSite.publication ?? "")) {
|
|
63
|
+
throw new Error("atmosphere-comments: `standardSite.publication` must be your publication's AT URI (at://did:…/site.standard.publication/…).");
|
|
64
|
+
}
|
|
60
65
|
const base = `/${(options.base ?? "/atmosphere").replace(/^\/+|\/+$/g, "")}`;
|
|
61
66
|
return {
|
|
62
67
|
name: "atmosphere-comments",
|
|
@@ -75,6 +80,7 @@ export default function atmosphereComments(options) {
|
|
|
75
80
|
logoUri: options.logoUri,
|
|
76
81
|
policyUri: options.policyUri,
|
|
77
82
|
tosUri: options.tosUri,
|
|
83
|
+
standardSite,
|
|
78
84
|
hidden: options.hidden ?? [],
|
|
79
85
|
locale: options.locale,
|
|
80
86
|
messages: options.messages ?? {},
|
|
@@ -105,11 +111,16 @@ export default function atmosphereComments(options) {
|
|
|
105
111
|
logoUri: settings.logoUri,
|
|
106
112
|
policyUri: settings.policyUri,
|
|
107
113
|
tosUri: settings.tosUri,
|
|
114
|
+
standardSite,
|
|
108
115
|
};
|
|
109
116
|
|
|
110
117
|
// The routes and components read the settings from virtual modules.
|
|
111
118
|
updateConfig({
|
|
112
119
|
vite: {
|
|
120
|
+
// Readers' browsers load the sign-in library only when needed, so
|
|
121
|
+
// Vite's dev server wouldn't find it to bundle it in advance, and
|
|
122
|
+
// the old-style modules it depends on then fail in the browser.
|
|
123
|
+
optimizeDeps: { include: ["astro-atmosphere-comments > @atproto/oauth-client-browser"] },
|
|
113
124
|
plugins: [
|
|
114
125
|
{
|
|
115
126
|
name: "atmosphere-comments-settings",
|