astro-atmosphere-comments 0.3.0 → 0.4.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/CHANGELOG.md +15 -0
- package/README.md +40 -2
- package/components/AtmosphereComments.astro +28 -1023
- package/components/AtmosphereReactions.astro +785 -0
- package/components/Composer.astro +80 -0
- package/components/SignInForm.astro +218 -0
- package/components/atmosphere.css +612 -0
- package/index.mjs +1 -0
- package/lib/client/composer.ts +232 -0
- package/lib/client/handle-suggestions.ts +112 -0
- package/lib/oauth.ts +5 -1
- package/locales/de.json +20 -0
- package/locales/en.json +20 -0
- package/locales/es.json +20 -0
- package/locales/fr.json +20 -0
- package/locales/it.json +20 -0
- package/locales/ja.json +20 -0
- package/locales/nl.json +20 -0
- package/locales/pt.json +20 -0
- package/package.json +7 -3
- package/routes/follow.ts +71 -0
- package/routes/login.ts +31 -5
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
// text, author, createdAt, quote, parent }), and after a like or repost (or
|
|
32
32
|
// undoing one) `atmosphere:reacted` (detail: { kind, on, author }), so a site
|
|
33
33
|
// can show it straight away.
|
|
34
|
-
import logo from "../assets/union.svg?url";
|
|
35
34
|
import settings from "../lib/settings";
|
|
35
|
+
import SignInForm from "./SignInForm.astro";
|
|
36
|
+
import Composer from "./Composer.astro";
|
|
37
|
+
import "./atmosphere.css";
|
|
36
38
|
import { messagesFor, pickLocale, translate, translateHtml } from "../lib/i18n";
|
|
37
39
|
import { escapeHtml } from "../lib/translate";
|
|
38
40
|
|
|
@@ -88,45 +90,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
88
90
|
<div class="atmo-reactions" data-reactions data-pagefind-ignore hidden></div>
|
|
89
91
|
|
|
90
92
|
<div class="atmo__signed-out" data-view="signed-out" data-pagefind-ignore>
|
|
91
|
-
<
|
|
92
|
-
<label for="atmo-handle">{t("signIn.heading")}</label>
|
|
93
|
-
<p
|
|
94
|
-
class="atmo__hint"
|
|
95
|
-
set:html={th("signIn.anyAccount", {
|
|
96
|
-
atmosphereAccount: link("https://atmosphereaccount.com", t("signIn.atmosphereAccount")),
|
|
97
|
-
bluesky: link("https://bsky.app", "Bluesky"),
|
|
98
|
-
})}
|
|
99
|
-
/>
|
|
100
|
-
<div class="atmo__row">
|
|
101
|
-
<span class="atmo__handle">
|
|
102
|
-
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
103
|
-
<input
|
|
104
|
-
id="atmo-handle"
|
|
105
|
-
name="handle"
|
|
106
|
-
type="text"
|
|
107
|
-
inputmode="url"
|
|
108
|
-
data-1p-ignore
|
|
109
|
-
data-lpignore="true"
|
|
110
|
-
data-bwignore
|
|
111
|
-
data-form-type="other"
|
|
112
|
-
autocapitalize="none"
|
|
113
|
-
spellcheck="false"
|
|
114
|
-
placeholder="alice.bsky.social"
|
|
115
|
-
required
|
|
116
|
-
/>
|
|
117
|
-
<ul class="atmo__suggestions" id="atmo-handle-suggestions" role="listbox" aria-label={t("signIn.matchingAccounts")} data-handle-suggestions hidden></ul>
|
|
118
|
-
</span>
|
|
119
|
-
<input type="hidden" name="returnTo" value={returnTo} />
|
|
120
|
-
<button type="submit" class="atmo__button atmo__button--sign-in">
|
|
121
|
-
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
122
|
-
{t("signIn.button")}
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
<p
|
|
126
|
-
class="atmo__hint"
|
|
127
|
-
set:html={th("signIn.privacy", { createAccount: link("https://atmosphereaccount.com", t("signIn.createAccount")) })}
|
|
128
|
-
/>
|
|
129
|
-
</form>
|
|
93
|
+
<SignInForm id="atmo" returnTo={returnTo} messages={messages} lang={lang} />
|
|
130
94
|
{blueskyLink && (
|
|
131
95
|
<p
|
|
132
96
|
class="atmo__elsewhere"
|
|
@@ -156,57 +120,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
156
120
|
</button>
|
|
157
121
|
</div>
|
|
158
122
|
|
|
159
|
-
<
|
|
160
|
-
<p class="atmo__context" data-context hidden>
|
|
161
|
-
<span data-context-text></span>
|
|
162
|
-
<button type="button" class="atmo__link-button" data-cancel-context>{t("composer.cancel")}</button>
|
|
163
|
-
</p>
|
|
164
|
-
<label for="atmo-text" class="atmo__as">
|
|
165
|
-
<span class="atmo__avatar atmo__avatar--me" data-me-avatar aria-hidden="true"></span>
|
|
166
|
-
<span set:html={th("composer.commentAs", { verb: `<span data-verb>${escapeHtml(t("composer.comment"))}</span>`, handle: "<strong data-handle></strong>" })} />
|
|
167
|
-
</label>
|
|
168
|
-
<div class="atmo__field">
|
|
169
|
-
<textarea
|
|
170
|
-
id="atmo-text"
|
|
171
|
-
name="text"
|
|
172
|
-
rows="4"
|
|
173
|
-
required
|
|
174
|
-
aria-autocomplete="list"
|
|
175
|
-
aria-controls="atmo-suggestions"
|
|
176
|
-
aria-expanded="false"></textarea>
|
|
177
|
-
<ul class="atmo__suggestions" id="atmo-suggestions" role="listbox" aria-label={t("composer.mentions")} data-suggestions hidden></ul>
|
|
178
|
-
</div>
|
|
179
|
-
<figure class="atmo__preview" data-preview hidden>
|
|
180
|
-
<figcaption>{t("composer.previewCaption")}</figcaption>
|
|
181
|
-
<div class="atmo__preview-post">
|
|
182
|
-
<span class="atmo__avatar" data-preview-avatar aria-hidden="true"></span>
|
|
183
|
-
<div class="atmo__preview-body">
|
|
184
|
-
<p class="atmo__preview-author"><strong data-preview-name></strong> <span data-preview-handle></span> <span>· {t("composer.now")}</span></p>
|
|
185
|
-
<p class="atmo__preview-text" data-preview-text></p>
|
|
186
|
-
<div class="atmo__quoted">
|
|
187
|
-
<p class="atmo__preview-author">
|
|
188
|
-
<span class="atmo__avatar atmo__avatar--small" data-quoted-avatar aria-hidden="true"></span>
|
|
189
|
-
<strong data-quoted-name></strong> <span data-quoted-handle></span>
|
|
190
|
-
</p>
|
|
191
|
-
<p class="atmo__preview-text" data-quoted-text></p>
|
|
192
|
-
<div class="atmo__card" data-card hidden>
|
|
193
|
-
<img data-card-image alt="" hidden />
|
|
194
|
-
<div>
|
|
195
|
-
<strong data-card-title></strong>
|
|
196
|
-
<span data-card-description></span>
|
|
197
|
-
<span class="atmo__card-domain" data-card-domain></span>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
</figure>
|
|
204
|
-
<div class="atmo__row atmo__row--end">
|
|
205
|
-
<span class="atmo__count" data-count aria-live="polite"></span>
|
|
206
|
-
<button type="button" class="atmo__link-button" data-sign-out>{t("composer.signOut")}</button>
|
|
207
|
-
<button type="submit" class="atmo__button" data-submit>{t("composer.postComment")}</button>
|
|
208
|
-
</div>
|
|
209
|
-
</form>
|
|
123
|
+
<Composer id="atmo" messages={messages} lang={lang} signOut />
|
|
210
124
|
</div>
|
|
211
125
|
|
|
212
126
|
<p class="atmo__status" data-status role="status" data-pagefind-ignore></p>
|
|
@@ -226,10 +140,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
226
140
|
|
|
227
141
|
<script>
|
|
228
142
|
import { translate } from "../lib/translate";
|
|
143
|
+
import { enhanceHandleBox } from "../lib/client/handle-suggestions";
|
|
144
|
+
import { MAX, quotePreview, setUpComposer, type ComposerBehaviour, type Posted } from "../lib/client/composer";
|
|
229
145
|
|
|
230
|
-
const MAX = 300;
|
|
231
|
-
const graphemes = new Intl.Segmenter();
|
|
232
|
-
const count = (text: string) => [...graphemes.segment(text.trim())].length;
|
|
233
146
|
|
|
234
147
|
// The error and status codes the routes and the sign-in round trip send,
|
|
235
148
|
// and the wording for each (in locales/*.json).
|
|
@@ -247,8 +160,6 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
247
160
|
error: "messages.error",
|
|
248
161
|
};
|
|
249
162
|
|
|
250
|
-
type Posted = { uri: string; url: string; quote: boolean };
|
|
251
|
-
type Suggestion = { handle: string; displayName?: string; avatar?: string };
|
|
252
163
|
|
|
253
164
|
class AtmosphereComments extends HTMLElement {
|
|
254
165
|
connectedCallback() {
|
|
@@ -434,7 +345,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
434
345
|
const threadReady = loadThread();
|
|
435
346
|
|
|
436
347
|
// A comment just posted, shown at once (Bluesky takes a moment to list it).
|
|
437
|
-
const showPosted = (data: Posted
|
|
348
|
+
const showPosted = (data: Posted) => {
|
|
438
349
|
if (data.quote) return;
|
|
439
350
|
const parent = data.parent
|
|
440
351
|
? repliesList.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(data.parent)}"]`)
|
|
@@ -471,98 +382,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
471
382
|
|
|
472
383
|
const openLogin = () => loginForm.querySelector("input")?.focus({ preventScroll: true });
|
|
473
384
|
|
|
474
|
-
|
|
475
|
-
// avatars, so they can see they've found the right one. Picking one signs
|
|
476
|
-
// in with it. Without JavaScript the box works as a plain text box.
|
|
477
|
-
{
|
|
478
|
-
const input = loginForm.querySelector<HTMLInputElement>("#atmo-handle")!;
|
|
479
|
-
const list = loginForm.querySelector<HTMLUListElement>("[data-handle-suggestions]")!;
|
|
480
|
-
let found: Suggestion[] = [];
|
|
481
|
-
let active = -1;
|
|
482
|
-
let latest = "";
|
|
483
|
-
let timer: number | undefined;
|
|
484
|
-
input.setAttribute("role", "combobox");
|
|
485
|
-
input.setAttribute("aria-autocomplete", "list");
|
|
486
|
-
input.setAttribute("aria-controls", list.id);
|
|
487
|
-
input.setAttribute("aria-expanded", "false");
|
|
488
|
-
|
|
489
|
-
const close = () => {
|
|
490
|
-
list.hidden = true;
|
|
491
|
-
list.replaceChildren();
|
|
492
|
-
found = [];
|
|
493
|
-
active = -1;
|
|
494
|
-
input.setAttribute("aria-expanded", "false");
|
|
495
|
-
input.removeAttribute("aria-activedescendant");
|
|
496
|
-
};
|
|
497
|
-
const highlight = (index: number) => {
|
|
498
|
-
active = index;
|
|
499
|
-
list.querySelectorAll("li").forEach((item, i) => item.setAttribute("aria-selected", String(i === index)));
|
|
500
|
-
if (index >= 0) input.setAttribute("aria-activedescendant", `${list.id}-option-${index}`);
|
|
501
|
-
else input.removeAttribute("aria-activedescendant");
|
|
502
|
-
};
|
|
503
|
-
const choose = (index: number) => {
|
|
504
|
-
const option = found[index];
|
|
505
|
-
if (!option) return;
|
|
506
|
-
input.value = option.handle;
|
|
507
|
-
close();
|
|
508
|
-
loginForm.requestSubmit();
|
|
509
|
-
};
|
|
510
|
-
const show = (actors: Suggestion[]) => {
|
|
511
|
-
found = actors.slice(0, 5);
|
|
512
|
-
if (!found.length || document.activeElement !== input) return close();
|
|
513
|
-
list.replaceChildren(
|
|
514
|
-
...found.map((option, index) => {
|
|
515
|
-
const item = make("li");
|
|
516
|
-
item.id = `${list.id}-option-${index}`;
|
|
517
|
-
item.setAttribute("role", "option");
|
|
518
|
-
const avatar = make("span", "atmo__avatar atmo__avatar--small");
|
|
519
|
-
avatar.setAttribute("aria-hidden", "true");
|
|
520
|
-
setAvatar(avatar, option.displayName || option.handle, option.avatar);
|
|
521
|
-
item.append(avatar, make("strong", "", option.displayName || option.handle), make("span", "", `@${option.handle}`));
|
|
522
|
-
// mousedown keeps the focus in the handle box.
|
|
523
|
-
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
524
|
-
item.addEventListener("click", () => choose(index));
|
|
525
|
-
return item;
|
|
526
|
-
}),
|
|
527
|
-
);
|
|
528
|
-
list.hidden = false;
|
|
529
|
-
input.setAttribute("aria-expanded", "true");
|
|
530
|
-
// Nothing is picked until the reader chooses, so Enter still signs in
|
|
531
|
-
// with exactly what they typed.
|
|
532
|
-
highlight(-1);
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
input.addEventListener("input", () => {
|
|
536
|
-
window.clearTimeout(timer);
|
|
537
|
-
const query = input.value.trim().replace(/^@/, "");
|
|
538
|
-
latest = query;
|
|
539
|
-
if (query.length < 2 || query.startsWith("did:") || /\s/.test(query)) return close();
|
|
540
|
-
timer = window.setTimeout(async () => {
|
|
541
|
-
try {
|
|
542
|
-
const response = await fetch(`${base}/typeahead?q=${encodeURIComponent(query)}`, { credentials: "same-origin" });
|
|
543
|
-
const data = (await response.json()) as { actors?: Suggestion[] };
|
|
544
|
-
if (query === latest) show(data.actors ?? []);
|
|
545
|
-
} catch {
|
|
546
|
-
close();
|
|
547
|
-
}
|
|
548
|
-
}, 250);
|
|
549
|
-
});
|
|
550
|
-
input.addEventListener("keydown", (event) => {
|
|
551
|
-
if (list.hidden) return;
|
|
552
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
553
|
-
event.preventDefault();
|
|
554
|
-
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
555
|
-
highlight(active < 0 && step < 0 ? found.length - 1 : (active + step + found.length) % found.length);
|
|
556
|
-
} else if (event.key === "Enter" && active >= 0) {
|
|
557
|
-
event.preventDefault();
|
|
558
|
-
choose(active);
|
|
559
|
-
} else if (event.key === "Escape") {
|
|
560
|
-
event.preventDefault();
|
|
561
|
-
close();
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
input.addEventListener("blur", () => window.setTimeout(close, 150));
|
|
565
|
-
}
|
|
385
|
+
enhanceHandleBox(loginForm, base);
|
|
566
386
|
|
|
567
387
|
// An avatar picture, or the name's first letter if there isn't one.
|
|
568
388
|
const setAvatar = (slot: HTMLElement, name: string, src?: string) => {
|
|
@@ -584,196 +404,26 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
584
404
|
Object.assign(document.createElement("a"), { href: data.url, textContent: t("posted.see") }),
|
|
585
405
|
);
|
|
586
406
|
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
const
|
|
407
|
+
// The comment box (lib/client/composer.ts): posts as a reply to the
|
|
408
|
+
// announcement, then shows it straight away and tells the page.
|
|
409
|
+
const composerContext = { base, t, message, setAvatar };
|
|
410
|
+
const composerFor = (
|
|
590
411
|
form: HTMLFormElement,
|
|
591
|
-
behaviour: {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const textarea = form.querySelector("textarea")!;
|
|
599
|
-
const counter = form.querySelector<HTMLElement>("[data-count]")!;
|
|
600
|
-
const submit = form.querySelector<HTMLButtonElement>("[data-submit]")!;
|
|
601
|
-
const suggestions = form.querySelector<HTMLUListElement>("[data-suggestions]")!;
|
|
602
|
-
|
|
603
|
-
const updateCount = () => {
|
|
604
|
-
const used = count(textarea.value);
|
|
605
|
-
counter.textContent = used > MAX - 50 ? t("composer.charactersLeft", { count: MAX - used }) : "";
|
|
606
|
-
counter.classList.toggle("is-over", used > MAX);
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
// @mention suggestions, as in the Bluesky app: typing @ and the start
|
|
610
|
-
// of a handle or name lists matching accounts to choose from.
|
|
611
|
-
let options: Suggestion[] = [];
|
|
612
|
-
let active = -1;
|
|
613
|
-
let lookup: number | undefined;
|
|
614
|
-
let latestQuery = "";
|
|
615
|
-
const mentionAtCaret = () => {
|
|
616
|
-
const before = textarea.value.slice(0, textarea.selectionStart);
|
|
617
|
-
const match = /(?:^|[\s(])@([a-zA-Z0-9.-]{1,64})$/.exec(before);
|
|
618
|
-
return match ? { query: match[1], start: before.length - match[1].length - 1 } : null;
|
|
619
|
-
};
|
|
620
|
-
const closeSuggestions = () => {
|
|
621
|
-
suggestions.hidden = true;
|
|
622
|
-
suggestions.replaceChildren();
|
|
623
|
-
options = [];
|
|
624
|
-
active = -1;
|
|
625
|
-
textarea.setAttribute("aria-expanded", "false");
|
|
626
|
-
textarea.removeAttribute("aria-activedescendant");
|
|
627
|
-
};
|
|
628
|
-
const highlight = (index: number) => {
|
|
629
|
-
active = index;
|
|
630
|
-
suggestions.querySelectorAll("li").forEach((item, i) => item.setAttribute("aria-selected", String(i === index)));
|
|
631
|
-
if (index >= 0) textarea.setAttribute("aria-activedescendant", `${suggestions.id}-option-${index}`);
|
|
632
|
-
};
|
|
633
|
-
const choose = (index: number) => {
|
|
634
|
-
const mention = mentionAtCaret();
|
|
635
|
-
const option = options[index];
|
|
636
|
-
if (!mention || !option) return closeSuggestions();
|
|
637
|
-
const after = textarea.value.slice(textarea.selectionStart);
|
|
638
|
-
textarea.value = `${textarea.value.slice(0, mention.start)}@${option.handle} ${after}`;
|
|
639
|
-
const caret = mention.start + option.handle.length + 2;
|
|
640
|
-
textarea.setSelectionRange(caret, caret);
|
|
641
|
-
closeSuggestions();
|
|
642
|
-
textarea.focus();
|
|
643
|
-
updateCount();
|
|
644
|
-
behaviour.onInput?.();
|
|
645
|
-
};
|
|
646
|
-
const showSuggestions = (found: Suggestion[]) => {
|
|
647
|
-
options = found;
|
|
648
|
-
if (!found.length) return closeSuggestions();
|
|
649
|
-
suggestions.replaceChildren(
|
|
650
|
-
...found.map((option, index) => {
|
|
651
|
-
const item = document.createElement("li");
|
|
652
|
-
item.id = `${suggestions.id}-option-${index}`;
|
|
653
|
-
item.setAttribute("role", "option");
|
|
654
|
-
const avatar = document.createElement("span");
|
|
655
|
-
avatar.className = "atmo__avatar atmo__avatar--small";
|
|
656
|
-
setAvatar(avatar, option.displayName || option.handle, option.avatar);
|
|
657
|
-
const name = Object.assign(document.createElement("strong"), { textContent: option.displayName || option.handle });
|
|
658
|
-
const handle = Object.assign(document.createElement("span"), { textContent: `@${option.handle}` });
|
|
659
|
-
item.append(avatar, name, handle);
|
|
660
|
-
// mousedown keeps the focus in the text box.
|
|
661
|
-
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
662
|
-
item.addEventListener("click", () => choose(index));
|
|
663
|
-
return item;
|
|
664
|
-
}),
|
|
665
|
-
);
|
|
666
|
-
suggestions.hidden = false;
|
|
667
|
-
textarea.setAttribute("aria-expanded", "true");
|
|
668
|
-
highlight(0);
|
|
669
|
-
};
|
|
670
|
-
const lookUpMention = () => {
|
|
671
|
-
window.clearTimeout(lookup);
|
|
672
|
-
const mention = mentionAtCaret();
|
|
673
|
-
if (!mention) return closeSuggestions();
|
|
674
|
-
latestQuery = mention.query;
|
|
675
|
-
lookup = window.setTimeout(async () => {
|
|
676
|
-
const query = mention.query;
|
|
677
|
-
try {
|
|
678
|
-
const response = await fetch(`${base}/typeahead?q=${encodeURIComponent(query)}`, { credentials: "same-origin" });
|
|
679
|
-
const data = (await response.json()) as { actors?: Suggestion[] };
|
|
680
|
-
if (query === latestQuery) showSuggestions(data.actors ?? []);
|
|
681
|
-
} catch {
|
|
682
|
-
closeSuggestions();
|
|
683
|
-
}
|
|
684
|
-
}, 200);
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
textarea.addEventListener("input", () => {
|
|
688
|
-
updateCount();
|
|
689
|
-
behaviour.onInput?.();
|
|
690
|
-
lookUpMention();
|
|
691
|
-
});
|
|
692
|
-
textarea.addEventListener("keydown", (event) => {
|
|
693
|
-
if (suggestions.hidden) return;
|
|
694
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
695
|
-
event.preventDefault();
|
|
696
|
-
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
697
|
-
highlight((active + step + options.length) % options.length);
|
|
698
|
-
} else if (event.key === "Enter" || event.key === "Tab") {
|
|
699
|
-
event.preventDefault();
|
|
700
|
-
choose(Math.max(active, 0));
|
|
701
|
-
} else if (event.key === "Escape") {
|
|
702
|
-
event.preventDefault();
|
|
703
|
-
closeSuggestions();
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
textarea.addEventListener("blur", () => window.setTimeout(closeSuggestions, 150));
|
|
707
|
-
|
|
708
|
-
form.addEventListener("submit", async (event) => {
|
|
709
|
-
event.preventDefault();
|
|
710
|
-
const text = textarea.value.trim();
|
|
711
|
-
if (!text) return;
|
|
712
|
-
if (count(text) > MAX) return behaviour.tell(message("too-long"));
|
|
713
|
-
submit.disabled = true;
|
|
714
|
-
behaviour.tell(t("composer.posting"));
|
|
715
|
-
try {
|
|
716
|
-
const response = await send("comment", { text, root: post, ...behaviour.extra() });
|
|
717
|
-
const data = await response.json();
|
|
718
|
-
if (!response.ok) return handleError(data.error, behaviour.tell);
|
|
719
|
-
textarea.value = "";
|
|
720
|
-
updateCount();
|
|
412
|
+
behaviour: Omit<ComposerBehaviour, "root" | "onError" | "onPosted"> & { onPosted: (data: Posted) => void },
|
|
413
|
+
) =>
|
|
414
|
+
setUpComposer(form, composerContext, {
|
|
415
|
+
...behaviour,
|
|
416
|
+
root: () => post,
|
|
417
|
+
onError: handleError,
|
|
418
|
+
onPosted: (data) => {
|
|
721
419
|
behaviour.onPosted(data);
|
|
722
420
|
showPosted(data);
|
|
723
421
|
this.dispatchEvent(new CustomEvent("atmosphere:commented", { detail: data, bubbles: true }));
|
|
724
|
-
}
|
|
725
|
-
behaviour.tell(message("error"));
|
|
726
|
-
} finally {
|
|
727
|
-
submit.disabled = false;
|
|
728
|
-
}
|
|
422
|
+
},
|
|
729
423
|
});
|
|
730
424
|
|
|
731
|
-
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// The Quote preview: the reader's words as they type, with the
|
|
735
|
-
// announcement quoted beneath, as Bluesky will show it.
|
|
425
|
+
// The Quote preview (lib/client/composer.ts).
|
|
736
426
|
const preview = $<HTMLElement>("[data-preview]");
|
|
737
|
-
const previewText = $<HTMLElement>("[data-preview-text]");
|
|
738
|
-
let quotedLoaded = false;
|
|
739
|
-
const updatePreviewText = () => {
|
|
740
|
-
const words = main.textarea.value.trim();
|
|
741
|
-
previewText.textContent = words || t("composer.previewEmpty");
|
|
742
|
-
previewText.classList.toggle("is-placeholder", !words);
|
|
743
|
-
};
|
|
744
|
-
const loadQuoted = async () => {
|
|
745
|
-
updatePreviewText();
|
|
746
|
-
if (quotedLoaded) return;
|
|
747
|
-
try {
|
|
748
|
-
const response = await fetch(`${base}/post?uri=${encodeURIComponent(post)}`);
|
|
749
|
-
if (!response.ok) return;
|
|
750
|
-
const quoted = (await response.json()) as {
|
|
751
|
-
author: { handle: string; displayName?: string; avatar?: string };
|
|
752
|
-
text: string;
|
|
753
|
-
external?: { uri: string; title: string; description: string; thumb?: string };
|
|
754
|
-
};
|
|
755
|
-
quotedLoaded = true;
|
|
756
|
-
const quotedName = quoted.author.displayName || quoted.author.handle;
|
|
757
|
-
$<HTMLElement>("[data-quoted-name]").textContent = quotedName;
|
|
758
|
-
setAvatar($<HTMLElement>("[data-quoted-avatar]"), quotedName, quoted.author.avatar);
|
|
759
|
-
$<HTMLElement>("[data-quoted-handle]").textContent = `@${quoted.author.handle}`;
|
|
760
|
-
$<HTMLElement>("[data-quoted-text]").textContent = quoted.text;
|
|
761
|
-
if (quoted.external) {
|
|
762
|
-
$<HTMLElement>("[data-card]").hidden = false;
|
|
763
|
-
$<HTMLElement>("[data-card-title]").textContent = quoted.external.title;
|
|
764
|
-
$<HTMLElement>("[data-card-description]").textContent = quoted.external.description;
|
|
765
|
-
$<HTMLElement>("[data-card-domain]").textContent = new URL(quoted.external.uri).hostname;
|
|
766
|
-
if (quoted.external.thumb) {
|
|
767
|
-
const img = $<HTMLImageElement>("[data-card-image]");
|
|
768
|
-
img.onload = () => (img.hidden = false);
|
|
769
|
-
img.onerror = () => (img.hidden = true);
|
|
770
|
-
img.src = quoted.external.thumb;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
} catch {
|
|
774
|
-
// The preview is a nicety: quoting still works without it.
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
427
|
|
|
778
428
|
// The main box: a comment on the post, or (after Quote) a quote post.
|
|
779
429
|
const setMainMode = (next: "comment" | "quote") => {
|
|
@@ -784,17 +434,18 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
784
434
|
$<HTMLElement>("[data-verb]").textContent = t(quoting ? "composer.quote" : "composer.comment");
|
|
785
435
|
$<HTMLElement>("[data-submit]").textContent = t(quoting ? "composer.postQuote" : "composer.postComment");
|
|
786
436
|
preview.hidden = !quoting;
|
|
787
|
-
if (quoting) void
|
|
437
|
+
if (quoting) void quote.load(post);
|
|
788
438
|
};
|
|
789
|
-
const main =
|
|
439
|
+
const main = composerFor(composer, {
|
|
790
440
|
extra: () => (mainMode === "quote" ? { quote: true } : {}),
|
|
791
441
|
tell: say,
|
|
792
442
|
onPosted: (data) => {
|
|
793
443
|
setMainMode("comment");
|
|
794
444
|
postedMessage(status, data);
|
|
795
445
|
},
|
|
796
|
-
onInput:
|
|
446
|
+
onInput: () => quote.update(),
|
|
797
447
|
});
|
|
448
|
+
const quote = quotePreview(composer, main.textarea, composerContext);
|
|
798
449
|
$<HTMLButtonElement>("[data-cancel-context]").addEventListener("click", () => {
|
|
799
450
|
setMainMode("comment");
|
|
800
451
|
main.textarea.focus();
|
|
@@ -825,7 +476,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
825
476
|
form.querySelector<HTMLElement>("[data-context]")!.hidden = false;
|
|
826
477
|
form.querySelector("[data-context-text]")!.textContent = t("composer.replyingTo", { name: author });
|
|
827
478
|
form.querySelector("[data-verb]")!.textContent = t("comment.reply");
|
|
828
|
-
form.querySelector("[data-submit]")!.textContent = "
|
|
479
|
+
form.querySelector("[data-submit]")!.textContent = t("composer.postReply");
|
|
829
480
|
form.querySelector("[data-handle]")!.textContent = me.handle ? `@${me.handle}` : "";
|
|
830
481
|
setAvatar(form.querySelector<HTMLElement>("[data-me-avatar]")!, me.displayName || me.handle || "?", me.avatar);
|
|
831
482
|
const replyStatus = scoped(Object.assign(document.createElement("p"), { className: "atmo__status" }));
|
|
@@ -834,7 +485,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
834
485
|
(comment.querySelector("article") ?? comment).append(holder);
|
|
835
486
|
const close = () => holder.remove();
|
|
836
487
|
form.querySelector("[data-cancel-context]")!.addEventListener("click", close);
|
|
837
|
-
|
|
488
|
+
composerFor(form, {
|
|
838
489
|
extra: () => ({ parent: comment.dataset.atmosphereUri }),
|
|
839
490
|
tell: (message) => (replyStatus.textContent = message),
|
|
840
491
|
// The reply appears under the comment (the page adds it), so the box goes.
|
|
@@ -1126,649 +777,3 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1126
777
|
|
|
1127
778
|
customElements.define("atmosphere-comments", AtmosphereComments);
|
|
1128
779
|
</script>
|
|
1129
|
-
|
|
1130
|
-
<style>
|
|
1131
|
-
.atmo {
|
|
1132
|
-
display: block;
|
|
1133
|
-
color: var(--atmo-text, currentColor);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/* Only the component's own parts wait for sign-in to be checked; the
|
|
1137
|
-
site's slot content shows straight away. */
|
|
1138
|
-
.atmo.is-loading > :is([data-view], [data-reactions], [data-replies]) {
|
|
1139
|
-
visibility: hidden;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
.atmo [hidden] {
|
|
1143
|
-
display: none !important;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
/* The Atmosphere Account logo inside the handle box, on the left, and at
|
|
1147
|
-
the start of the sign-in button. It is single-colour, so it is drawn in
|
|
1148
|
-
the muted text colour in the box and the button's text colour on it. */
|
|
1149
|
-
.atmo__handle {
|
|
1150
|
-
position: relative;
|
|
1151
|
-
display: flex;
|
|
1152
|
-
flex: 1 1 220px;
|
|
1153
|
-
min-width: 0;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
/* Matching accounts sit under the handle box, as wide as it. */
|
|
1157
|
-
.atmo__handle .atmo__suggestions {
|
|
1158
|
-
width: 100%;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
.atmo__handle .atmo__logo {
|
|
1162
|
-
position: absolute;
|
|
1163
|
-
top: 50%;
|
|
1164
|
-
left: 11px;
|
|
1165
|
-
width: 20px;
|
|
1166
|
-
height: 20px;
|
|
1167
|
-
transform: translateY(-50%);
|
|
1168
|
-
background-color: var(--atmo-muted, currentColor);
|
|
1169
|
-
mask: var(--atmo-logo) center / contain no-repeat;
|
|
1170
|
-
pointer-events: none;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
.atmo .atmo__handle input[type="text"] {
|
|
1174
|
-
padding-left: 40px;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
.atmo__button--sign-in {
|
|
1178
|
-
display: inline-flex;
|
|
1179
|
-
align-items: center;
|
|
1180
|
-
gap: 8px;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
.atmo__button--sign-in .atmo__logo {
|
|
1184
|
-
flex: none;
|
|
1185
|
-
width: 20px;
|
|
1186
|
-
height: 20px;
|
|
1187
|
-
background-color: currentColor;
|
|
1188
|
-
mask: var(--atmo-logo) center / contain no-repeat;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
/* The comment box when it sits under a comment being replied to. */
|
|
1192
|
-
.atmo--inline {
|
|
1193
|
-
margin: 4px 0 6px;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
.atmo--inline .atmo__composer {
|
|
1197
|
-
margin-top: 10px;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
.atmo__elsewhere {
|
|
1201
|
-
max-width: 560px;
|
|
1202
|
-
margin: 14px 0 0;
|
|
1203
|
-
font-size: 0.9em;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
.atmo__elsewhere :global(.atmo__elsewhere-link) {
|
|
1207
|
-
font-weight: 700;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
.atmo__login,
|
|
1211
|
-
.atmo__composer {
|
|
1212
|
-
display: grid;
|
|
1213
|
-
gap: 8px;
|
|
1214
|
-
max-width: 560px;
|
|
1215
|
-
margin-top: 14px;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
.atmo label {
|
|
1219
|
-
font-weight: 700;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
.atmo__row {
|
|
1223
|
-
display: flex;
|
|
1224
|
-
flex-wrap: wrap;
|
|
1225
|
-
align-items: center;
|
|
1226
|
-
gap: 10px;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
.atmo__row--end {
|
|
1230
|
-
justify-content: flex-end;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
.atmo input[type="text"],
|
|
1234
|
-
.atmo textarea {
|
|
1235
|
-
flex: 1 1 220px;
|
|
1236
|
-
min-width: 0;
|
|
1237
|
-
padding: 10px 12px;
|
|
1238
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1239
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1240
|
-
background: var(--atmo-field-bg, transparent);
|
|
1241
|
-
color: inherit;
|
|
1242
|
-
font: inherit;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
.atmo textarea {
|
|
1246
|
-
resize: vertical;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
.atmo__button {
|
|
1250
|
-
padding: 10px 16px;
|
|
1251
|
-
border: 0;
|
|
1252
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1253
|
-
background: var(--atmo-accent, #1185fe);
|
|
1254
|
-
color: var(--atmo-on-accent, #fff);
|
|
1255
|
-
font: inherit;
|
|
1256
|
-
font-weight: 700;
|
|
1257
|
-
cursor: pointer;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
.atmo__button:disabled {
|
|
1261
|
-
opacity: 0.6;
|
|
1262
|
-
cursor: progress;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
.atmo__link-button {
|
|
1266
|
-
padding: 0;
|
|
1267
|
-
border: 0;
|
|
1268
|
-
background: none;
|
|
1269
|
-
color: var(--atmo-muted, inherit);
|
|
1270
|
-
font: inherit;
|
|
1271
|
-
text-decoration: underline;
|
|
1272
|
-
cursor: pointer;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
/* Like, Repost and Quote: outlined pills that fill in once done. */
|
|
1276
|
-
.atmo__actions {
|
|
1277
|
-
display: flex;
|
|
1278
|
-
flex-wrap: wrap;
|
|
1279
|
-
gap: 8px;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
.atmo__action {
|
|
1283
|
-
display: inline-flex;
|
|
1284
|
-
align-items: center;
|
|
1285
|
-
gap: 6px;
|
|
1286
|
-
padding: 7px 14px 7px 10px;
|
|
1287
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1288
|
-
border-radius: 999px;
|
|
1289
|
-
background: none;
|
|
1290
|
-
color: inherit;
|
|
1291
|
-
font: inherit;
|
|
1292
|
-
font-weight: 600;
|
|
1293
|
-
cursor: pointer;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
.atmo__action:hover,
|
|
1297
|
-
.atmo__action:focus-visible {
|
|
1298
|
-
border-color: var(--atmo-accent, #1185fe);
|
|
1299
|
-
color: var(--atmo-accent, #1185fe);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
.atmo__action svg {
|
|
1303
|
-
width: 18px;
|
|
1304
|
-
height: 18px;
|
|
1305
|
-
fill: none;
|
|
1306
|
-
stroke: currentColor;
|
|
1307
|
-
stroke-width: 1.8;
|
|
1308
|
-
stroke-linecap: round;
|
|
1309
|
-
stroke-linejoin: round;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
.atmo__action[aria-pressed="true"] {
|
|
1313
|
-
border-color: var(--atmo-accent, #1185fe);
|
|
1314
|
-
color: var(--atmo-accent, #1185fe);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
.atmo__action[data-react="like"][aria-pressed="true"] svg {
|
|
1318
|
-
fill: currentColor;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
.atmo__action [data-label-on],
|
|
1322
|
-
.atmo__action[aria-pressed="true"] [data-label-off] {
|
|
1323
|
-
display: none;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
.atmo__action[aria-pressed="true"] [data-label-on] {
|
|
1327
|
-
display: inline;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
.atmo__context {
|
|
1331
|
-
display: flex;
|
|
1332
|
-
flex-wrap: wrap;
|
|
1333
|
-
gap: 10px;
|
|
1334
|
-
margin: 0;
|
|
1335
|
-
color: var(--atmo-muted, inherit);
|
|
1336
|
-
font-size: 0.9em;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
.atmo__hint,
|
|
1340
|
-
.atmo__count,
|
|
1341
|
-
.atmo__status {
|
|
1342
|
-
margin: 0;
|
|
1343
|
-
color: var(--atmo-muted, inherit);
|
|
1344
|
-
font-size: 0.9em;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
.atmo__count {
|
|
1348
|
-
margin-right: auto;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
.atmo__count.is-over {
|
|
1352
|
-
color: var(--atmo-error, #d33);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
.atmo__status:not(:empty) {
|
|
1356
|
-
margin-top: 10px;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
.atmo__credit {
|
|
1360
|
-
margin: 14px 0 0;
|
|
1361
|
-
color: var(--atmo-muted, inherit);
|
|
1362
|
-
font-size: 0.75em;
|
|
1363
|
-
opacity: 0.8;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
.atmo__credit :global(a) {
|
|
1367
|
-
color: inherit;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
/* @mention suggestions, under the text box. */
|
|
1371
|
-
.atmo__field {
|
|
1372
|
-
position: relative;
|
|
1373
|
-
display: grid;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
.atmo__suggestions {
|
|
1377
|
-
position: absolute;
|
|
1378
|
-
z-index: 5;
|
|
1379
|
-
top: calc(100% + 4px);
|
|
1380
|
-
left: 0;
|
|
1381
|
-
width: min(100%, 360px);
|
|
1382
|
-
margin: 0;
|
|
1383
|
-
padding: 4px;
|
|
1384
|
-
list-style: none;
|
|
1385
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1386
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1387
|
-
background: var(--atmo-menu-bg, Canvas);
|
|
1388
|
-
box-shadow: 0 12px 30px rgb(0 0 0 / 0.25);
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
.atmo__suggestions :global(li) {
|
|
1392
|
-
display: flex;
|
|
1393
|
-
align-items: center;
|
|
1394
|
-
gap: 8px;
|
|
1395
|
-
padding: 7px 9px;
|
|
1396
|
-
border-radius: calc(var(--atmo-radius, 10px) - 4px);
|
|
1397
|
-
cursor: pointer;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
.atmo__suggestions :global(li[aria-selected="true"]),
|
|
1401
|
-
.atmo__suggestions :global(li:hover) {
|
|
1402
|
-
background: color-mix(in srgb, var(--atmo-accent, #1185fe) 14%, transparent);
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
.atmo__suggestions :global(li span:last-child) {
|
|
1406
|
-
overflow: hidden;
|
|
1407
|
-
color: var(--atmo-muted, inherit);
|
|
1408
|
-
text-overflow: ellipsis;
|
|
1409
|
-
white-space: nowrap;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
.atmo__suggestions :global(.atmo__avatar) {
|
|
1413
|
-
display: grid;
|
|
1414
|
-
flex: none;
|
|
1415
|
-
place-items: center;
|
|
1416
|
-
width: 26px;
|
|
1417
|
-
height: 26px;
|
|
1418
|
-
overflow: hidden;
|
|
1419
|
-
border-radius: 50%;
|
|
1420
|
-
background: var(--atmo-border, #8886);
|
|
1421
|
-
font-size: 11px;
|
|
1422
|
-
font-weight: 700;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
.atmo__suggestions :global(.atmo__avatar img) {
|
|
1426
|
-
width: 100%;
|
|
1427
|
-
height: 100%;
|
|
1428
|
-
object-fit: cover;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
/* The Quote preview: a post card with the quoted post nested inside. */
|
|
1432
|
-
.atmo__preview {
|
|
1433
|
-
display: grid;
|
|
1434
|
-
gap: 6px;
|
|
1435
|
-
margin: 4px 0 0;
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
.atmo__preview figcaption {
|
|
1439
|
-
color: var(--atmo-muted, inherit);
|
|
1440
|
-
font-size: 0.85em;
|
|
1441
|
-
font-weight: 700;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
.atmo__preview-post,
|
|
1445
|
-
.atmo__quoted {
|
|
1446
|
-
padding: 12px 14px;
|
|
1447
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1448
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
/* As on Bluesky: the avatar beside the post, the post's parts stacked. */
|
|
1452
|
-
.atmo__preview-post {
|
|
1453
|
-
display: grid;
|
|
1454
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
1455
|
-
gap: 12px;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
.atmo__preview-body,
|
|
1459
|
-
.atmo__quoted {
|
|
1460
|
-
display: grid;
|
|
1461
|
-
gap: 6px;
|
|
1462
|
-
min-width: 0;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
.atmo__avatar {
|
|
1466
|
-
display: grid;
|
|
1467
|
-
place-items: center;
|
|
1468
|
-
width: 42px;
|
|
1469
|
-
height: 42px;
|
|
1470
|
-
overflow: hidden;
|
|
1471
|
-
border-radius: 50%;
|
|
1472
|
-
background: var(--atmo-border, #8886);
|
|
1473
|
-
color: var(--atmo-text, inherit);
|
|
1474
|
-
font-weight: 700;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
/* "Comment as @handle", with the reader's avatar as a reminder of which
|
|
1478
|
-
account they're signed in with. */
|
|
1479
|
-
.atmo__as {
|
|
1480
|
-
display: flex;
|
|
1481
|
-
align-items: center;
|
|
1482
|
-
gap: 8px;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
.atmo__avatar--me {
|
|
1486
|
-
width: 28px;
|
|
1487
|
-
height: 28px;
|
|
1488
|
-
font-size: 12px;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
.atmo__avatar--small {
|
|
1492
|
-
display: inline-grid;
|
|
1493
|
-
width: 18px;
|
|
1494
|
-
height: 18px;
|
|
1495
|
-
margin-right: 4px;
|
|
1496
|
-
font-size: 10px;
|
|
1497
|
-
vertical-align: -4px;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
/* Added by the script, so outside Astro's scoped styles. */
|
|
1501
|
-
.atmo__avatar :global(img) {
|
|
1502
|
-
width: 100%;
|
|
1503
|
-
height: 100%;
|
|
1504
|
-
object-fit: cover;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
.atmo__preview-post p,
|
|
1508
|
-
.atmo__card span {
|
|
1509
|
-
margin: 0;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
.atmo__preview-author span,
|
|
1513
|
-
.atmo__card-domain {
|
|
1514
|
-
color: var(--atmo-muted, inherit);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
.atmo__preview-text {
|
|
1518
|
-
white-space: pre-wrap;
|
|
1519
|
-
overflow-wrap: anywhere;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
.atmo__preview-text.is-placeholder {
|
|
1523
|
-
color: var(--atmo-muted, inherit);
|
|
1524
|
-
font-style: italic;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
.atmo__quoted {
|
|
1528
|
-
font-size: 0.92em;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
.atmo__card {
|
|
1532
|
-
overflow: hidden;
|
|
1533
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1534
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
.atmo__card img {
|
|
1538
|
-
display: block;
|
|
1539
|
-
width: 100%;
|
|
1540
|
-
aspect-ratio: 1.91 / 1;
|
|
1541
|
-
object-fit: cover;
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
.atmo__card > div {
|
|
1545
|
-
display: grid;
|
|
1546
|
-
gap: 2px;
|
|
1547
|
-
padding: 10px 12px;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
.atmo__card [data-card-description] {
|
|
1551
|
-
display: -webkit-box;
|
|
1552
|
-
overflow: hidden;
|
|
1553
|
-
-webkit-box-orient: vertical;
|
|
1554
|
-
-webkit-line-clamp: 2;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
.atmo__card-domain {
|
|
1558
|
-
font-size: 0.85em;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
/* The likes and reposts, and the comments. These are drawn by the script,
|
|
1562
|
-
and wrapped in :where() so they carry no weight: any rule a site writes
|
|
1563
|
-
for these classes wins. */
|
|
1564
|
-
:global(:where(.atmo-reactions)) {
|
|
1565
|
-
display: flex;
|
|
1566
|
-
flex-wrap: wrap;
|
|
1567
|
-
gap: 8px 32px;
|
|
1568
|
-
margin: 0 0 18px;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
:global(:where(.atmo-reactions__count)) {
|
|
1572
|
-
margin: 0 0 8px;
|
|
1573
|
-
font-weight: 700;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
:global(:where(.atmo-facepile)) {
|
|
1577
|
-
display: flex;
|
|
1578
|
-
flex-wrap: wrap;
|
|
1579
|
-
margin: 0;
|
|
1580
|
-
padding: 0 0 0 8px;
|
|
1581
|
-
list-style: none;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
:global(:where(.atmo-facepile li)) {
|
|
1585
|
-
margin-left: -8px;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
:global(:where(.atmo-facepile a)) {
|
|
1589
|
-
display: block;
|
|
1590
|
-
border-radius: 50%;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
:global(:where(.atmo-facepile__avatar, .atmo-facepile__more)) {
|
|
1594
|
-
display: grid;
|
|
1595
|
-
place-items: center;
|
|
1596
|
-
width: 36px;
|
|
1597
|
-
height: 36px;
|
|
1598
|
-
border: 2px solid var(--atmo-ring, Canvas);
|
|
1599
|
-
border-radius: 50%;
|
|
1600
|
-
object-fit: cover;
|
|
1601
|
-
background: var(--atmo-field-bg, #8882);
|
|
1602
|
-
font-size: 12px;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
:global(:where(.atmo-facepile__more)) {
|
|
1606
|
-
width: auto;
|
|
1607
|
-
min-width: 36px;
|
|
1608
|
-
padding: 0 8px;
|
|
1609
|
-
border-radius: 18px;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
:global(:where(.atmo-avatar-initial)) {
|
|
1613
|
-
display: inline-grid;
|
|
1614
|
-
place-items: center;
|
|
1615
|
-
color: #fff;
|
|
1616
|
-
font-weight: 800;
|
|
1617
|
-
background: var(--atmo-accent, #1185fe);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
:global(:where(.atmo-comments)) {
|
|
1621
|
-
display: grid;
|
|
1622
|
-
gap: 12px;
|
|
1623
|
-
margin: 24px 0 0;
|
|
1624
|
-
padding: 0;
|
|
1625
|
-
list-style: none;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
:global(:where(.atmo-comments:empty)) {
|
|
1629
|
-
display: none;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
:global(:where(.atmo-comment article)) {
|
|
1633
|
-
padding: 16px 18px;
|
|
1634
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1635
|
-
border-radius: 16px;
|
|
1636
|
-
background: var(--atmo-card-bg, transparent);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
:global(:where(.atmo-comment--author > article)) {
|
|
1640
|
-
border-color: var(--atmo-accent, #1185fe);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
/* Replies step in under the comment they answer (up to three levels). */
|
|
1644
|
-
:global(:where(.atmo-comment--nested)) {
|
|
1645
|
-
margin-left: calc((var(--depth, 1) - 1) * 24px);
|
|
1646
|
-
padding-left: 22px;
|
|
1647
|
-
border-left: 2px solid var(--atmo-border, #8886);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
:global(:where(.atmo-comment__header)) {
|
|
1651
|
-
display: flex;
|
|
1652
|
-
align-items: center;
|
|
1653
|
-
gap: 12px;
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
:global(:where(.atmo-comment__avatar)) {
|
|
1657
|
-
flex: none;
|
|
1658
|
-
width: 40px;
|
|
1659
|
-
height: 40px;
|
|
1660
|
-
border-radius: 50%;
|
|
1661
|
-
object-fit: cover;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
:global(:where(.atmo-comment__who)) {
|
|
1665
|
-
display: flex;
|
|
1666
|
-
flex-wrap: wrap;
|
|
1667
|
-
align-items: center;
|
|
1668
|
-
gap: 8px;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
:global(:where(.atmo-comment__author)) {
|
|
1672
|
-
color: inherit;
|
|
1673
|
-
font-weight: 750;
|
|
1674
|
-
text-decoration: none;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
:global(:where(.atmo-comment__badge)) {
|
|
1678
|
-
padding: 2px 7px;
|
|
1679
|
-
border: 1px solid var(--atmo-accent, #1185fe);
|
|
1680
|
-
border-radius: 999px;
|
|
1681
|
-
color: var(--atmo-accent, #1185fe);
|
|
1682
|
-
font-size: 11px;
|
|
1683
|
-
font-weight: 800;
|
|
1684
|
-
letter-spacing: 0.04em;
|
|
1685
|
-
text-transform: uppercase;
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
:global(:where(.atmo-comment__date)) {
|
|
1689
|
-
color: var(--atmo-muted, inherit);
|
|
1690
|
-
font-size: 13px;
|
|
1691
|
-
text-decoration: none;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
:global(:where(.atmo-comment__body)) {
|
|
1695
|
-
margin-top: 10px;
|
|
1696
|
-
line-height: 1.65;
|
|
1697
|
-
overflow-wrap: anywhere;
|
|
1698
|
-
white-space: pre-line;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
:global(:where(.atmo-comment__body p)) {
|
|
1702
|
-
margin: 0;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
/* Reply, Like and Repost under each comment (outside this element, so
|
|
1706
|
-
unscoped). */
|
|
1707
|
-
:global(.atmo-comment-actions) {
|
|
1708
|
-
display: flex;
|
|
1709
|
-
flex-wrap: wrap;
|
|
1710
|
-
gap: 18px;
|
|
1711
|
-
margin-top: 8px;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
:global(.atmo-comment-delete) {
|
|
1715
|
-
display: flex;
|
|
1716
|
-
flex-wrap: wrap;
|
|
1717
|
-
align-items: center;
|
|
1718
|
-
gap: 6px 12px;
|
|
1719
|
-
margin-top: 8px;
|
|
1720
|
-
font-size: 0.9em;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
:global(.atmo-comment-delete form) {
|
|
1724
|
-
display: contents;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
:global(.atmo-comment-delete__text) {
|
|
1728
|
-
color: var(--atmo-muted, inherit);
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
:global(.atmo-comment-action) {
|
|
1733
|
-
display: inline-flex;
|
|
1734
|
-
align-items: center;
|
|
1735
|
-
gap: 5px;
|
|
1736
|
-
padding: 0;
|
|
1737
|
-
border: 0;
|
|
1738
|
-
background: none;
|
|
1739
|
-
color: var(--atmo-muted, inherit);
|
|
1740
|
-
font: inherit;
|
|
1741
|
-
font-size: 0.85em;
|
|
1742
|
-
font-weight: 600;
|
|
1743
|
-
cursor: pointer;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
:global(.atmo-comment-action--delete) {
|
|
1747
|
-
margin-left: auto;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
:global(.atmo-comment-action--danger) {
|
|
1751
|
-
color: var(--atmo-danger, #c0392b);
|
|
1752
|
-
font-weight: 700;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
:global(.atmo-comment-action:hover),
|
|
1756
|
-
:global(.atmo-comment-action:focus-visible),
|
|
1757
|
-
:global(.atmo-comment-action[aria-pressed="true"]) {
|
|
1758
|
-
color: var(--atmo-accent, #1185fe);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
:global(.atmo-comment-action svg) {
|
|
1762
|
-
width: 16px;
|
|
1763
|
-
height: 16px;
|
|
1764
|
-
fill: none;
|
|
1765
|
-
stroke: currentColor;
|
|
1766
|
-
stroke-width: 1.8;
|
|
1767
|
-
stroke-linecap: round;
|
|
1768
|
-
stroke-linejoin: round;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
:global(.atmo-comment-action[data-kind="like"][aria-pressed="true"] svg) {
|
|
1772
|
-
fill: currentColor;
|
|
1773
|
-
}
|
|
1774
|
-
</style>
|