astro-atmosphere-comments 0.2.0 → 0.4.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 +17 -0
- package/README.md +59 -3
- package/components/AtmosphereComments.astro +120 -1087
- package/components/AtmosphereReactions.astro +785 -0
- package/components/Composer.astro +80 -0
- package/components/SignInForm.astro +218 -0
- package/components/atmosphere.css +603 -0
- package/index.d.ts +11 -0
- package/index.mjs +3 -0
- package/lib/client/composer.ts +232 -0
- package/lib/client/handle-suggestions.ts +112 -0
- package/lib/i18n.ts +32 -0
- package/lib/oauth.ts +5 -1
- package/lib/settings.ts +2 -0
- package/lib/translate.ts +36 -0
- package/locales/de.json +95 -0
- package/locales/en.json +95 -0
- package/locales/es.json +98 -0
- package/locales/fr.json +98 -0
- package/locales/it.json +98 -0
- package/locales/ja.json +92 -0
- package/locales/nl.json +95 -0
- package/locales/pt.json +98 -0
- package/package.json +8 -3
- package/routes/follow.ts +71 -0
- package/routes/login.ts +31 -5
|
@@ -31,8 +31,12 @@
|
|
|
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";
|
|
38
|
+
import { messagesFor, pickLocale, translate, translateHtml } from "../lib/i18n";
|
|
39
|
+
import { escapeHtml } from "../lib/translate";
|
|
36
40
|
|
|
37
41
|
interface Props {
|
|
38
42
|
/**
|
|
@@ -49,15 +53,26 @@ interface Props {
|
|
|
49
53
|
credit?: boolean;
|
|
50
54
|
/** "Rather not sign in here? Reply on Bluesky instead →" under the sign-in form. Default true. */
|
|
51
55
|
blueskyLink?: boolean;
|
|
56
|
+
/** The language of the wording, e.g. "fr". Overrides the integration's `locale`. */
|
|
57
|
+
locale?: string;
|
|
58
|
+
/** Your own wording for any line, by its key in locales/en.json. */
|
|
59
|
+
messages?: Record<string, string>;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
const {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const { post = "", label, authorLabel, credit = true, blueskyLink = true } = Astro.props;
|
|
63
|
+
// The wording: the language asked for (or Astro's i18n locale for the page),
|
|
64
|
+
// with the site's own lines on top. The script gets the same, as JSON.
|
|
65
|
+
const lang = pickLocale(Astro.props.locale, settings.locale, Astro.currentLocale);
|
|
66
|
+
const messages = messagesFor(lang, {
|
|
67
|
+
...settings.messages,
|
|
68
|
+
...Astro.props.messages,
|
|
69
|
+
...(label && { "signIn.heading": label }),
|
|
70
|
+
...(authorLabel && { "comment.author": authorLabel }),
|
|
71
|
+
});
|
|
72
|
+
const t = (key: string, vars?: Record<string, string | number>) => translate(messages, lang, key, vars);
|
|
73
|
+
const th = (key: string, html: Record<string, string>) => translateHtml(messages, lang, key, html);
|
|
74
|
+
const link = (href: string | undefined, text: string, attributes = "") =>
|
|
75
|
+
`<a${href ? ` href="${escapeHtml(href)}"` : ""}${attributes}>${escapeHtml(text)}</a>`;
|
|
61
76
|
// The announcement on bsky.app, when the page is given it (otherwise the
|
|
62
77
|
// script fills it in once the thread has loaded).
|
|
63
78
|
const atUri = post.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/);
|
|
@@ -71,121 +86,41 @@ const { base } = settings;
|
|
|
71
86
|
const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
72
87
|
---
|
|
73
88
|
|
|
74
|
-
<atmosphere-comments class="atmo" id="atmosphere-comments" data-post={post} data-base={base} data-
|
|
89
|
+
<atmosphere-comments class="atmo" id="atmosphere-comments" data-post={post} data-base={base} data-locale={lang} data-messages={JSON.stringify(messages)}>
|
|
75
90
|
<div class="atmo-reactions" data-reactions data-pagefind-ignore hidden></div>
|
|
76
91
|
|
|
77
92
|
<div class="atmo__signed-out" data-view="signed-out" data-pagefind-ignore>
|
|
78
|
-
<
|
|
79
|
-
<label for="atmo-handle">{label}</label>
|
|
80
|
-
<p class="atmo__hint">Any <a href="https://atmosphereaccount.com">Atmosphere account</a> works, including <a href="https://bsky.app">Bluesky</a>: one account for a whole network of open apps.</p>
|
|
81
|
-
<div class="atmo__row">
|
|
82
|
-
<span class="atmo__handle">
|
|
83
|
-
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
84
|
-
<input
|
|
85
|
-
id="atmo-handle"
|
|
86
|
-
name="handle"
|
|
87
|
-
type="text"
|
|
88
|
-
inputmode="url"
|
|
89
|
-
data-1p-ignore
|
|
90
|
-
data-lpignore="true"
|
|
91
|
-
data-bwignore
|
|
92
|
-
data-form-type="other"
|
|
93
|
-
autocapitalize="none"
|
|
94
|
-
spellcheck="false"
|
|
95
|
-
placeholder="alice.bsky.social"
|
|
96
|
-
required
|
|
97
|
-
/>
|
|
98
|
-
<ul class="atmo__suggestions" id="atmo-handle-suggestions" role="listbox" aria-label="Matching accounts" data-handle-suggestions hidden></ul>
|
|
99
|
-
</span>
|
|
100
|
-
<input type="hidden" name="returnTo" value={returnTo} />
|
|
101
|
-
<button type="submit" class="atmo__button atmo__button--sign-in">
|
|
102
|
-
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
103
|
-
Sign in with Atmosphere
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
<p class="atmo__hint">
|
|
107
|
-
You'll approve this on your account's own site (Bluesky's, for most people); your password
|
|
108
|
-
is never shared here.
|
|
109
|
-
No account yet? <a href="https://atmosphereaccount.com">Create a free Atmosphere account</a>.
|
|
110
|
-
</p>
|
|
111
|
-
</form>
|
|
93
|
+
<SignInForm id="atmo" returnTo={returnTo} messages={messages} lang={lang} />
|
|
112
94
|
{blueskyLink && (
|
|
113
|
-
<p
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
<p
|
|
96
|
+
class="atmo__elsewhere"
|
|
97
|
+
data-elsewhere
|
|
98
|
+
hidden={!blueskyUrl}
|
|
99
|
+
set:html={th("elsewhere.text", {
|
|
100
|
+
// u-syndication: microformats readers learn this is the post's copy on Bluesky.
|
|
101
|
+
link: link(blueskyUrl || undefined, t("elsewhere.link"), ' class="atmo__elsewhere-link u-syndication" data-elsewhere-link'),
|
|
102
|
+
})}
|
|
103
|
+
/>
|
|
119
104
|
)}
|
|
120
105
|
</div>
|
|
121
106
|
|
|
122
107
|
<div class="atmo__signed-in" data-view="signed-in" data-pagefind-ignore hidden>
|
|
123
|
-
<div class="atmo__actions" role="group" aria-label="
|
|
108
|
+
<div class="atmo__actions" role="group" aria-label={t("actions.label")}>
|
|
124
109
|
<button type="button" class="atmo__action" data-react="like" aria-pressed="false">
|
|
125
110
|
<svg viewBox="0 0 24 24" aria-hidden="true"><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" /></svg>
|
|
126
|
-
<span data-label-off>
|
|
111
|
+
<span data-label-off>{t("actions.like")}</span><span data-label-on>{t("actions.liked")}</span>
|
|
127
112
|
</button>
|
|
128
113
|
<button type="button" class="atmo__action" data-react="repost" aria-pressed="false">
|
|
129
114
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 8h10l-3-3M17 16H7l3 3M5 13V9.5A1.5 1.5 0 0 1 6.5 8M19 11v3.5a1.5 1.5 0 0 1-1.5 1.5" /></svg>
|
|
130
|
-
<span data-label-off>
|
|
115
|
+
<span data-label-off>{t("actions.repost")}</span><span data-label-on>{t("actions.reposted")}</span>
|
|
131
116
|
</button>
|
|
132
117
|
<button type="button" class="atmo__action" data-quote>
|
|
133
118
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 18h5v-5H6.5A4.5 4.5 0 0 1 11 8.5V7a6 6 0 0 0-6 6Zm9 0h5v-5h-3.5A4.5 4.5 0 0 1 20 8.5V7a6 6 0 0 0-6 6Z" /></svg>
|
|
134
|
-
<span>
|
|
119
|
+
<span>{t("actions.quote")}</span>
|
|
135
120
|
</button>
|
|
136
121
|
</div>
|
|
137
122
|
|
|
138
|
-
<
|
|
139
|
-
<p class="atmo__context" data-context hidden>
|
|
140
|
-
<span data-context-text></span>
|
|
141
|
-
<button type="button" class="atmo__link-button" data-cancel-context>Cancel</button>
|
|
142
|
-
</p>
|
|
143
|
-
<label for="atmo-text" class="atmo__as">
|
|
144
|
-
<span class="atmo__avatar atmo__avatar--me" data-me-avatar aria-hidden="true"></span>
|
|
145
|
-
<span><span data-verb>Comment</span> as <strong data-handle></strong></span>
|
|
146
|
-
</label>
|
|
147
|
-
<div class="atmo__field">
|
|
148
|
-
<textarea
|
|
149
|
-
id="atmo-text"
|
|
150
|
-
name="text"
|
|
151
|
-
rows="4"
|
|
152
|
-
required
|
|
153
|
-
aria-autocomplete="list"
|
|
154
|
-
aria-controls="atmo-suggestions"
|
|
155
|
-
aria-expanded="false"></textarea>
|
|
156
|
-
<ul class="atmo__suggestions" id="atmo-suggestions" role="listbox" aria-label="Accounts to mention" data-suggestions hidden></ul>
|
|
157
|
-
</div>
|
|
158
|
-
<figure class="atmo__preview" data-preview hidden>
|
|
159
|
-
<figcaption>How your post will look</figcaption>
|
|
160
|
-
<div class="atmo__preview-post">
|
|
161
|
-
<span class="atmo__avatar" data-preview-avatar aria-hidden="true"></span>
|
|
162
|
-
<div class="atmo__preview-body">
|
|
163
|
-
<p class="atmo__preview-author"><strong data-preview-name></strong> <span data-preview-handle></span> <span>· now</span></p>
|
|
164
|
-
<p class="atmo__preview-text" data-preview-text></p>
|
|
165
|
-
<div class="atmo__quoted">
|
|
166
|
-
<p class="atmo__preview-author">
|
|
167
|
-
<span class="atmo__avatar atmo__avatar--small" data-quoted-avatar aria-hidden="true"></span>
|
|
168
|
-
<strong data-quoted-name></strong> <span data-quoted-handle></span>
|
|
169
|
-
</p>
|
|
170
|
-
<p class="atmo__preview-text" data-quoted-text></p>
|
|
171
|
-
<div class="atmo__card" data-card hidden>
|
|
172
|
-
<img data-card-image alt="" hidden />
|
|
173
|
-
<div>
|
|
174
|
-
<strong data-card-title></strong>
|
|
175
|
-
<span data-card-description></span>
|
|
176
|
-
<span class="atmo__card-domain" data-card-domain></span>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
</figure>
|
|
183
|
-
<div class="atmo__row atmo__row--end">
|
|
184
|
-
<span class="atmo__count" data-count aria-live="polite"></span>
|
|
185
|
-
<button type="button" class="atmo__link-button" data-sign-out>Sign out</button>
|
|
186
|
-
<button type="submit" class="atmo__button" data-submit>Post comment</button>
|
|
187
|
-
</div>
|
|
188
|
-
</form>
|
|
123
|
+
<Composer id="atmo" messages={messages} lang={lang} signOut />
|
|
189
124
|
</div>
|
|
190
125
|
|
|
191
126
|
<p class="atmo__status" data-status role="status" data-pagefind-ignore></p>
|
|
@@ -199,37 +134,41 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
199
134
|
<slot />
|
|
200
135
|
|
|
201
136
|
{credit && (
|
|
202
|
-
<p class="atmo__credit" data-credit data-pagefind-ignore
|
|
203
|
-
Powered by <a href={CREDIT_URL}>Atmosphere Comments for Astro</a>
|
|
204
|
-
</p>
|
|
137
|
+
<p class="atmo__credit" data-credit data-pagefind-ignore set:html={th("credit", { link: link(CREDIT_URL, "Atmosphere Comments for Astro") })} />
|
|
205
138
|
)}
|
|
206
139
|
</atmosphere-comments>
|
|
207
140
|
|
|
208
141
|
<script>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
|
|
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";
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
// The error and status codes the routes and the sign-in round trip send,
|
|
148
|
+
// and the wording for each (in locales/*.json).
|
|
149
|
+
const MESSAGE_KEYS: Record<string, string> = {
|
|
150
|
+
"signed-in": "messages.signedIn",
|
|
151
|
+
"sign-in-failed": "messages.signInFailed",
|
|
152
|
+
"account-not-found": "messages.accountNotFound",
|
|
153
|
+
"invalid-handle": "messages.invalidHandle",
|
|
154
|
+
"signed-out": "messages.signedOut",
|
|
155
|
+
"needs-permission": "messages.needsPermission",
|
|
156
|
+
"not-allowed": "messages.notAllowed",
|
|
157
|
+
"new-permissions": "messages.newPermissions",
|
|
158
|
+
"too-long": "messages.tooLong",
|
|
159
|
+
"too-many": "messages.tooMany",
|
|
160
|
+
error: "messages.error",
|
|
225
161
|
};
|
|
226
162
|
|
|
227
|
-
type Posted = { uri: string; url: string; quote: boolean };
|
|
228
|
-
type Suggestion = { handle: string; displayName?: string; avatar?: string };
|
|
229
163
|
|
|
230
164
|
class AtmosphereComments extends HTMLElement {
|
|
231
165
|
connectedCallback() {
|
|
232
166
|
const base = this.dataset.base ?? "/atmosphere";
|
|
167
|
+
// The wording, in the page's language, from the server.
|
|
168
|
+
const lang = this.dataset.locale || "en";
|
|
169
|
+
const wording = JSON.parse(this.dataset.messages || "{}") as Record<string, string>;
|
|
170
|
+
const t = (key: string, vars?: Record<string, string | number>) => translate(wording, lang, key, vars);
|
|
171
|
+
const message = (code: string) => t(MESSAGE_KEYS[code] ?? MESSAGE_KEYS.error, { max: MAX });
|
|
233
172
|
// The announcement's AT URI. A bsky.app address or the page's
|
|
234
173
|
// Standard.site record is turned into one when the thread loads.
|
|
235
174
|
let post = this.dataset.post?.startsWith("at://") ? this.dataset.post : "";
|
|
@@ -281,9 +220,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
281
220
|
let thread: Thread | null = null;
|
|
282
221
|
const reactionsBox = $<HTMLElement>("[data-reactions]");
|
|
283
222
|
const repliesList = $<HTMLOListElement>("[data-replies]");
|
|
284
|
-
const authorLabel =
|
|
223
|
+
const authorLabel = t("comment.author");
|
|
285
224
|
const nameOf = (person: Pick<Person, "handle" | "displayName">) => person.displayName || `@${person.handle}`;
|
|
286
|
-
const dates = new Intl.DateTimeFormat(
|
|
225
|
+
const dates = new Intl.DateTimeFormat(lang, {
|
|
287
226
|
day: "numeric",
|
|
288
227
|
month: "long",
|
|
289
228
|
year: "numeric",
|
|
@@ -312,7 +251,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
312
251
|
...groups.map(({ kind, count, people }) => {
|
|
313
252
|
const group = make("div", "atmo-reactions__group");
|
|
314
253
|
group.dataset.reaction = kind;
|
|
315
|
-
group.append(make("p", "atmo-reactions__count",
|
|
254
|
+
group.append(make("p", "atmo-reactions__count", t(kind === "like" ? "reactions.likes" : "reactions.reposts", { count })));
|
|
316
255
|
const faces = make("ul", "atmo-facepile");
|
|
317
256
|
for (const person of people) {
|
|
318
257
|
const link = Object.assign(make("a"), { href: person.url, title: nameOf(person), rel: "ugc nofollow noopener" });
|
|
@@ -345,7 +284,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
345
284
|
if (reply.byAuthor) who.append(make("span", "atmo-comment__badge", authorLabel));
|
|
346
285
|
const date = Object.assign(make("a", "atmo-comment__date"), { href: reply.url, rel: "ugc nofollow noopener" });
|
|
347
286
|
const time = Object.assign(make("time"), { dateTime: reply.createdAt, textContent: reply.createdAt ? dates.format(new Date(reply.createdAt)) : "" });
|
|
348
|
-
date.append(time, make("span", "",
|
|
287
|
+
date.append(time, make("span", "", ` · ${t("comment.viewOnBluesky")}`));
|
|
349
288
|
const names = make("div");
|
|
350
289
|
names.append(who, date);
|
|
351
290
|
const header = make("header", "atmo-comment__header");
|
|
@@ -406,7 +345,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
406
345
|
const threadReady = loadThread();
|
|
407
346
|
|
|
408
347
|
// A comment just posted, shown at once (Bluesky takes a moment to list it).
|
|
409
|
-
const showPosted = (data: Posted
|
|
348
|
+
const showPosted = (data: Posted) => {
|
|
410
349
|
if (data.quote) return;
|
|
411
350
|
const parent = data.parent
|
|
412
351
|
? repliesList.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(data.parent)}"]`)
|
|
@@ -443,98 +382,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
443
382
|
|
|
444
383
|
const openLogin = () => loginForm.querySelector("input")?.focus({ preventScroll: true });
|
|
445
384
|
|
|
446
|
-
|
|
447
|
-
// avatars, so they can see they've found the right one. Picking one signs
|
|
448
|
-
// in with it. Without JavaScript the box works as a plain text box.
|
|
449
|
-
{
|
|
450
|
-
const input = loginForm.querySelector<HTMLInputElement>("#atmo-handle")!;
|
|
451
|
-
const list = loginForm.querySelector<HTMLUListElement>("[data-handle-suggestions]")!;
|
|
452
|
-
let found: Suggestion[] = [];
|
|
453
|
-
let active = -1;
|
|
454
|
-
let latest = "";
|
|
455
|
-
let timer: number | undefined;
|
|
456
|
-
input.setAttribute("role", "combobox");
|
|
457
|
-
input.setAttribute("aria-autocomplete", "list");
|
|
458
|
-
input.setAttribute("aria-controls", list.id);
|
|
459
|
-
input.setAttribute("aria-expanded", "false");
|
|
460
|
-
|
|
461
|
-
const close = () => {
|
|
462
|
-
list.hidden = true;
|
|
463
|
-
list.replaceChildren();
|
|
464
|
-
found = [];
|
|
465
|
-
active = -1;
|
|
466
|
-
input.setAttribute("aria-expanded", "false");
|
|
467
|
-
input.removeAttribute("aria-activedescendant");
|
|
468
|
-
};
|
|
469
|
-
const highlight = (index: number) => {
|
|
470
|
-
active = index;
|
|
471
|
-
list.querySelectorAll("li").forEach((item, i) => item.setAttribute("aria-selected", String(i === index)));
|
|
472
|
-
if (index >= 0) input.setAttribute("aria-activedescendant", `${list.id}-option-${index}`);
|
|
473
|
-
else input.removeAttribute("aria-activedescendant");
|
|
474
|
-
};
|
|
475
|
-
const choose = (index: number) => {
|
|
476
|
-
const option = found[index];
|
|
477
|
-
if (!option) return;
|
|
478
|
-
input.value = option.handle;
|
|
479
|
-
close();
|
|
480
|
-
loginForm.requestSubmit();
|
|
481
|
-
};
|
|
482
|
-
const show = (actors: Suggestion[]) => {
|
|
483
|
-
found = actors.slice(0, 5);
|
|
484
|
-
if (!found.length || document.activeElement !== input) return close();
|
|
485
|
-
list.replaceChildren(
|
|
486
|
-
...found.map((option, index) => {
|
|
487
|
-
const item = make("li");
|
|
488
|
-
item.id = `${list.id}-option-${index}`;
|
|
489
|
-
item.setAttribute("role", "option");
|
|
490
|
-
const avatar = make("span", "atmo__avatar atmo__avatar--small");
|
|
491
|
-
avatar.setAttribute("aria-hidden", "true");
|
|
492
|
-
setAvatar(avatar, option.displayName || option.handle, option.avatar);
|
|
493
|
-
item.append(avatar, make("strong", "", option.displayName || option.handle), make("span", "", `@${option.handle}`));
|
|
494
|
-
// mousedown keeps the focus in the handle box.
|
|
495
|
-
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
496
|
-
item.addEventListener("click", () => choose(index));
|
|
497
|
-
return item;
|
|
498
|
-
}),
|
|
499
|
-
);
|
|
500
|
-
list.hidden = false;
|
|
501
|
-
input.setAttribute("aria-expanded", "true");
|
|
502
|
-
// Nothing is picked until the reader chooses, so Enter still signs in
|
|
503
|
-
// with exactly what they typed.
|
|
504
|
-
highlight(-1);
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
input.addEventListener("input", () => {
|
|
508
|
-
window.clearTimeout(timer);
|
|
509
|
-
const query = input.value.trim().replace(/^@/, "");
|
|
510
|
-
latest = query;
|
|
511
|
-
if (query.length < 2 || query.startsWith("did:") || /\s/.test(query)) return close();
|
|
512
|
-
timer = window.setTimeout(async () => {
|
|
513
|
-
try {
|
|
514
|
-
const response = await fetch(`${base}/typeahead?q=${encodeURIComponent(query)}`, { credentials: "same-origin" });
|
|
515
|
-
const data = (await response.json()) as { actors?: Suggestion[] };
|
|
516
|
-
if (query === latest) show(data.actors ?? []);
|
|
517
|
-
} catch {
|
|
518
|
-
close();
|
|
519
|
-
}
|
|
520
|
-
}, 250);
|
|
521
|
-
});
|
|
522
|
-
input.addEventListener("keydown", (event) => {
|
|
523
|
-
if (list.hidden) return;
|
|
524
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
525
|
-
event.preventDefault();
|
|
526
|
-
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
527
|
-
highlight(active < 0 && step < 0 ? found.length - 1 : (active + step + found.length) % found.length);
|
|
528
|
-
} else if (event.key === "Enter" && active >= 0) {
|
|
529
|
-
event.preventDefault();
|
|
530
|
-
choose(active);
|
|
531
|
-
} else if (event.key === "Escape") {
|
|
532
|
-
event.preventDefault();
|
|
533
|
-
close();
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
input.addEventListener("blur", () => window.setTimeout(close, 150));
|
|
537
|
-
}
|
|
385
|
+
enhanceHandleBox(loginForm, base);
|
|
538
386
|
|
|
539
387
|
// An avatar picture, or the name's first letter if there isn't one.
|
|
540
388
|
const setAvatar = (slot: HTMLElement, name: string, src?: string) => {
|
|
@@ -547,226 +395,57 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
547
395
|
|
|
548
396
|
const handleError = (error: string, tell: (message: string) => void = say) => {
|
|
549
397
|
if (error === "signed-out" || error === "new-permissions") void show(false);
|
|
550
|
-
tell(
|
|
398
|
+
tell(message(error));
|
|
551
399
|
};
|
|
552
400
|
|
|
553
401
|
const postedMessage = (where: HTMLElement, data: Posted) =>
|
|
554
402
|
where.replaceChildren(
|
|
555
|
-
data.quote ? "
|
|
556
|
-
Object.assign(document.createElement("a"), { href: data.url, textContent: "
|
|
403
|
+
`${t(data.quote ? "posted.quote" : "posted.comment")} `,
|
|
404
|
+
Object.assign(document.createElement("a"), { href: data.url, textContent: t("posted.see") }),
|
|
557
405
|
);
|
|
558
406
|
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
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 = (
|
|
562
411
|
form: HTMLFormElement,
|
|
563
|
-
behaviour: {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const textarea = form.querySelector("textarea")!;
|
|
571
|
-
const counter = form.querySelector<HTMLElement>("[data-count]")!;
|
|
572
|
-
const submit = form.querySelector<HTMLButtonElement>("[data-submit]")!;
|
|
573
|
-
const suggestions = form.querySelector<HTMLUListElement>("[data-suggestions]")!;
|
|
574
|
-
|
|
575
|
-
const updateCount = () => {
|
|
576
|
-
const used = count(textarea.value);
|
|
577
|
-
counter.textContent = used > MAX - 50 ? `${MAX - used} left` : "";
|
|
578
|
-
counter.classList.toggle("is-over", used > MAX);
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
// @mention suggestions, as in the Bluesky app: typing @ and the start
|
|
582
|
-
// of a handle or name lists matching accounts to choose from.
|
|
583
|
-
let options: Suggestion[] = [];
|
|
584
|
-
let active = -1;
|
|
585
|
-
let lookup: number | undefined;
|
|
586
|
-
let latestQuery = "";
|
|
587
|
-
const mentionAtCaret = () => {
|
|
588
|
-
const before = textarea.value.slice(0, textarea.selectionStart);
|
|
589
|
-
const match = /(?:^|[\s(])@([a-zA-Z0-9.-]{1,64})$/.exec(before);
|
|
590
|
-
return match ? { query: match[1], start: before.length - match[1].length - 1 } : null;
|
|
591
|
-
};
|
|
592
|
-
const closeSuggestions = () => {
|
|
593
|
-
suggestions.hidden = true;
|
|
594
|
-
suggestions.replaceChildren();
|
|
595
|
-
options = [];
|
|
596
|
-
active = -1;
|
|
597
|
-
textarea.setAttribute("aria-expanded", "false");
|
|
598
|
-
textarea.removeAttribute("aria-activedescendant");
|
|
599
|
-
};
|
|
600
|
-
const highlight = (index: number) => {
|
|
601
|
-
active = index;
|
|
602
|
-
suggestions.querySelectorAll("li").forEach((item, i) => item.setAttribute("aria-selected", String(i === index)));
|
|
603
|
-
if (index >= 0) textarea.setAttribute("aria-activedescendant", `${suggestions.id}-option-${index}`);
|
|
604
|
-
};
|
|
605
|
-
const choose = (index: number) => {
|
|
606
|
-
const mention = mentionAtCaret();
|
|
607
|
-
const option = options[index];
|
|
608
|
-
if (!mention || !option) return closeSuggestions();
|
|
609
|
-
const after = textarea.value.slice(textarea.selectionStart);
|
|
610
|
-
textarea.value = `${textarea.value.slice(0, mention.start)}@${option.handle} ${after}`;
|
|
611
|
-
const caret = mention.start + option.handle.length + 2;
|
|
612
|
-
textarea.setSelectionRange(caret, caret);
|
|
613
|
-
closeSuggestions();
|
|
614
|
-
textarea.focus();
|
|
615
|
-
updateCount();
|
|
616
|
-
behaviour.onInput?.();
|
|
617
|
-
};
|
|
618
|
-
const showSuggestions = (found: Suggestion[]) => {
|
|
619
|
-
options = found;
|
|
620
|
-
if (!found.length) return closeSuggestions();
|
|
621
|
-
suggestions.replaceChildren(
|
|
622
|
-
...found.map((option, index) => {
|
|
623
|
-
const item = document.createElement("li");
|
|
624
|
-
item.id = `${suggestions.id}-option-${index}`;
|
|
625
|
-
item.setAttribute("role", "option");
|
|
626
|
-
const avatar = document.createElement("span");
|
|
627
|
-
avatar.className = "atmo__avatar atmo__avatar--small";
|
|
628
|
-
setAvatar(avatar, option.displayName || option.handle, option.avatar);
|
|
629
|
-
const name = Object.assign(document.createElement("strong"), { textContent: option.displayName || option.handle });
|
|
630
|
-
const handle = Object.assign(document.createElement("span"), { textContent: `@${option.handle}` });
|
|
631
|
-
item.append(avatar, name, handle);
|
|
632
|
-
// mousedown keeps the focus in the text box.
|
|
633
|
-
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
634
|
-
item.addEventListener("click", () => choose(index));
|
|
635
|
-
return item;
|
|
636
|
-
}),
|
|
637
|
-
);
|
|
638
|
-
suggestions.hidden = false;
|
|
639
|
-
textarea.setAttribute("aria-expanded", "true");
|
|
640
|
-
highlight(0);
|
|
641
|
-
};
|
|
642
|
-
const lookUpMention = () => {
|
|
643
|
-
window.clearTimeout(lookup);
|
|
644
|
-
const mention = mentionAtCaret();
|
|
645
|
-
if (!mention) return closeSuggestions();
|
|
646
|
-
latestQuery = mention.query;
|
|
647
|
-
lookup = window.setTimeout(async () => {
|
|
648
|
-
const query = mention.query;
|
|
649
|
-
try {
|
|
650
|
-
const response = await fetch(`${base}/typeahead?q=${encodeURIComponent(query)}`, { credentials: "same-origin" });
|
|
651
|
-
const data = (await response.json()) as { actors?: Suggestion[] };
|
|
652
|
-
if (query === latestQuery) showSuggestions(data.actors ?? []);
|
|
653
|
-
} catch {
|
|
654
|
-
closeSuggestions();
|
|
655
|
-
}
|
|
656
|
-
}, 200);
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
textarea.addEventListener("input", () => {
|
|
660
|
-
updateCount();
|
|
661
|
-
behaviour.onInput?.();
|
|
662
|
-
lookUpMention();
|
|
663
|
-
});
|
|
664
|
-
textarea.addEventListener("keydown", (event) => {
|
|
665
|
-
if (suggestions.hidden) return;
|
|
666
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
667
|
-
event.preventDefault();
|
|
668
|
-
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
669
|
-
highlight((active + step + options.length) % options.length);
|
|
670
|
-
} else if (event.key === "Enter" || event.key === "Tab") {
|
|
671
|
-
event.preventDefault();
|
|
672
|
-
choose(Math.max(active, 0));
|
|
673
|
-
} else if (event.key === "Escape") {
|
|
674
|
-
event.preventDefault();
|
|
675
|
-
closeSuggestions();
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
textarea.addEventListener("blur", () => window.setTimeout(closeSuggestions, 150));
|
|
679
|
-
|
|
680
|
-
form.addEventListener("submit", async (event) => {
|
|
681
|
-
event.preventDefault();
|
|
682
|
-
const text = textarea.value.trim();
|
|
683
|
-
if (!text) return;
|
|
684
|
-
if (count(text) > MAX) return behaviour.tell(MESSAGES["too-long"]);
|
|
685
|
-
submit.disabled = true;
|
|
686
|
-
behaviour.tell("Posting…");
|
|
687
|
-
try {
|
|
688
|
-
const response = await send("comment", { text, root: post, ...behaviour.extra() });
|
|
689
|
-
const data = await response.json();
|
|
690
|
-
if (!response.ok) return handleError(data.error, behaviour.tell);
|
|
691
|
-
textarea.value = "";
|
|
692
|
-
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) => {
|
|
693
419
|
behaviour.onPosted(data);
|
|
694
420
|
showPosted(data);
|
|
695
421
|
this.dispatchEvent(new CustomEvent("atmosphere:commented", { detail: data, bubbles: true }));
|
|
696
|
-
}
|
|
697
|
-
behaviour.tell(MESSAGES.error);
|
|
698
|
-
} finally {
|
|
699
|
-
submit.disabled = false;
|
|
700
|
-
}
|
|
422
|
+
},
|
|
701
423
|
});
|
|
702
424
|
|
|
703
|
-
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
// The Quote preview: the reader's words as they type, with the
|
|
707
|
-
// announcement quoted beneath, as Bluesky will show it.
|
|
425
|
+
// The Quote preview (lib/client/composer.ts).
|
|
708
426
|
const preview = $<HTMLElement>("[data-preview]");
|
|
709
|
-
const previewText = $<HTMLElement>("[data-preview-text]");
|
|
710
|
-
let quotedLoaded = false;
|
|
711
|
-
const updatePreviewText = () => {
|
|
712
|
-
const words = main.textarea.value.trim();
|
|
713
|
-
previewText.textContent = words || "Your words appear here.";
|
|
714
|
-
previewText.classList.toggle("is-placeholder", !words);
|
|
715
|
-
};
|
|
716
|
-
const loadQuoted = async () => {
|
|
717
|
-
updatePreviewText();
|
|
718
|
-
if (quotedLoaded) return;
|
|
719
|
-
try {
|
|
720
|
-
const response = await fetch(`${base}/post?uri=${encodeURIComponent(post)}`);
|
|
721
|
-
if (!response.ok) return;
|
|
722
|
-
const quoted = (await response.json()) as {
|
|
723
|
-
author: { handle: string; displayName?: string; avatar?: string };
|
|
724
|
-
text: string;
|
|
725
|
-
external?: { uri: string; title: string; description: string; thumb?: string };
|
|
726
|
-
};
|
|
727
|
-
quotedLoaded = true;
|
|
728
|
-
const quotedName = quoted.author.displayName || quoted.author.handle;
|
|
729
|
-
$<HTMLElement>("[data-quoted-name]").textContent = quotedName;
|
|
730
|
-
setAvatar($<HTMLElement>("[data-quoted-avatar]"), quotedName, quoted.author.avatar);
|
|
731
|
-
$<HTMLElement>("[data-quoted-handle]").textContent = `@${quoted.author.handle}`;
|
|
732
|
-
$<HTMLElement>("[data-quoted-text]").textContent = quoted.text;
|
|
733
|
-
if (quoted.external) {
|
|
734
|
-
$<HTMLElement>("[data-card]").hidden = false;
|
|
735
|
-
$<HTMLElement>("[data-card-title]").textContent = quoted.external.title;
|
|
736
|
-
$<HTMLElement>("[data-card-description]").textContent = quoted.external.description;
|
|
737
|
-
$<HTMLElement>("[data-card-domain]").textContent = new URL(quoted.external.uri).hostname;
|
|
738
|
-
if (quoted.external.thumb) {
|
|
739
|
-
const img = $<HTMLImageElement>("[data-card-image]");
|
|
740
|
-
img.onload = () => (img.hidden = false);
|
|
741
|
-
img.onerror = () => (img.hidden = true);
|
|
742
|
-
img.src = quoted.external.thumb;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
} catch {
|
|
746
|
-
// The preview is a nicety: quoting still works without it.
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
427
|
|
|
750
428
|
// The main box: a comment on the post, or (after Quote) a quote post.
|
|
751
429
|
const setMainMode = (next: "comment" | "quote") => {
|
|
752
430
|
mainMode = next;
|
|
753
431
|
const quoting = next === "quote";
|
|
754
432
|
$<HTMLElement>("[data-context]").hidden = !quoting;
|
|
755
|
-
$<HTMLElement>("[data-context-text]").textContent = quoting ? "
|
|
756
|
-
$<HTMLElement>("[data-verb]").textContent = quoting ? "
|
|
757
|
-
$<HTMLElement>("[data-submit]").textContent = quoting ? "
|
|
433
|
+
$<HTMLElement>("[data-context-text]").textContent = quoting ? t("composer.quoting") : "";
|
|
434
|
+
$<HTMLElement>("[data-verb]").textContent = t(quoting ? "composer.quote" : "composer.comment");
|
|
435
|
+
$<HTMLElement>("[data-submit]").textContent = t(quoting ? "composer.postQuote" : "composer.postComment");
|
|
758
436
|
preview.hidden = !quoting;
|
|
759
|
-
if (quoting) void
|
|
437
|
+
if (quoting) void quote.load(post);
|
|
760
438
|
};
|
|
761
|
-
const main =
|
|
439
|
+
const main = composerFor(composer, {
|
|
762
440
|
extra: () => (mainMode === "quote" ? { quote: true } : {}),
|
|
763
441
|
tell: say,
|
|
764
442
|
onPosted: (data) => {
|
|
765
443
|
setMainMode("comment");
|
|
766
444
|
postedMessage(status, data);
|
|
767
445
|
},
|
|
768
|
-
onInput:
|
|
446
|
+
onInput: () => quote.update(),
|
|
769
447
|
});
|
|
448
|
+
const quote = quotePreview(composer, main.textarea, composerContext);
|
|
770
449
|
$<HTMLButtonElement>("[data-cancel-context]").addEventListener("click", () => {
|
|
771
450
|
setMainMode("comment");
|
|
772
451
|
main.textarea.focus();
|
|
@@ -795,9 +474,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
795
474
|
suggestions.id = `${id}-suggestions`;
|
|
796
475
|
textarea.setAttribute("aria-controls", suggestions.id);
|
|
797
476
|
form.querySelector<HTMLElement>("[data-context]")!.hidden = false;
|
|
798
|
-
form.querySelector("[data-context-text]")!.textContent =
|
|
799
|
-
form.querySelector("[data-verb]")!.textContent = "
|
|
800
|
-
form.querySelector("[data-submit]")!.textContent = "
|
|
477
|
+
form.querySelector("[data-context-text]")!.textContent = t("composer.replyingTo", { name: author });
|
|
478
|
+
form.querySelector("[data-verb]")!.textContent = t("comment.reply");
|
|
479
|
+
form.querySelector("[data-submit]")!.textContent = t("composer.postReply");
|
|
801
480
|
form.querySelector("[data-handle]")!.textContent = me.handle ? `@${me.handle}` : "";
|
|
802
481
|
setAvatar(form.querySelector<HTMLElement>("[data-me-avatar]")!, me.displayName || me.handle || "?", me.avatar);
|
|
803
482
|
const replyStatus = scoped(Object.assign(document.createElement("p"), { className: "atmo__status" }));
|
|
@@ -806,7 +485,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
806
485
|
(comment.querySelector("article") ?? comment).append(holder);
|
|
807
486
|
const close = () => holder.remove();
|
|
808
487
|
form.querySelector("[data-cancel-context]")!.addEventListener("click", close);
|
|
809
|
-
|
|
488
|
+
composerFor(form, {
|
|
810
489
|
extra: () => ({ parent: comment.dataset.atmosphereUri }),
|
|
811
490
|
tell: (message) => (replyStatus.textContent = message),
|
|
812
491
|
// The reply appears under the comment (the page adds it), so the box goes.
|
|
@@ -835,8 +514,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
835
514
|
button.dataset.kind = kind;
|
|
836
515
|
const seen = known[comment.dataset.atmosphereUri!];
|
|
837
516
|
button.setAttribute("aria-pressed", String(!!(kind === "like" ? seen?.liked : seen?.reposted)));
|
|
838
|
-
const author = comment.dataset.atmosphereAuthor || "
|
|
839
|
-
button.setAttribute("aria-label",
|
|
517
|
+
const author = comment.dataset.atmosphereAuthor || t("comment.someone");
|
|
518
|
+
button.setAttribute("aria-label", t(kind === "like" ? "comment.likeLabel" : "comment.repostLabel", { name: author }));
|
|
840
519
|
const count = Number(comment.dataset[kind === "like" ? "atmosphereLikes" : "atmosphereReposts"] || 0);
|
|
841
520
|
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${kind === "like" ? HEART : ARROWS}</svg><span></span>`;
|
|
842
521
|
const setCount = (value: number) => {
|
|
@@ -845,7 +524,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
845
524
|
};
|
|
846
525
|
setCount(count);
|
|
847
526
|
button.addEventListener("click", async () => {
|
|
848
|
-
if (!isSignedIn) return signInFirst(
|
|
527
|
+
if (!isSignedIn) return signInFirst(t(kind === "like" ? "signInFirst.like" : "signInFirst.repost"));
|
|
849
528
|
const on = button.getAttribute("aria-pressed") !== "true";
|
|
850
529
|
const before = Number(button.dataset.count || 0);
|
|
851
530
|
button.setAttribute("aria-pressed", String(on));
|
|
@@ -869,15 +548,15 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
869
548
|
if (comment.querySelector(":scope .atmo-comment-actions")) return;
|
|
870
549
|
added += 1;
|
|
871
550
|
const actions = Object.assign(document.createElement("div"), { className: "atmo-comment-actions" });
|
|
872
|
-
const author = comment.dataset.atmosphereAuthor || "
|
|
551
|
+
const author = comment.dataset.atmosphereAuthor || t("comment.someone");
|
|
873
552
|
const reply = Object.assign(document.createElement("button"), {
|
|
874
553
|
type: "button",
|
|
875
554
|
className: "atmo-comment-action",
|
|
876
|
-
textContent: "
|
|
555
|
+
textContent: t("comment.reply"),
|
|
877
556
|
});
|
|
878
|
-
reply.setAttribute("aria-label",
|
|
557
|
+
reply.setAttribute("aria-label", t("comment.replyTo", { name: author }));
|
|
879
558
|
reply.addEventListener("click", () => {
|
|
880
|
-
if (!isSignedIn) return signInFirst("
|
|
559
|
+
if (!isSignedIn) return signInFirst(t("signInFirst.reply"));
|
|
881
560
|
openReply(comment, author);
|
|
882
561
|
});
|
|
883
562
|
actions.append(reply, commentButton("like", comment), commentButton("repost", comment));
|
|
@@ -892,13 +571,13 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
892
571
|
|
|
893
572
|
const deleteComment = async (comment: HTMLElement, panel: HTMLElement) => {
|
|
894
573
|
const uri = comment.dataset.atmosphereUri!;
|
|
895
|
-
panel.replaceChildren(make("span", "atmo-comment-delete__text", "
|
|
574
|
+
panel.replaceChildren(make("span", "atmo-comment-delete__text", t("comment.deleting")));
|
|
896
575
|
try {
|
|
897
576
|
const response = await send("delete", { post: uri });
|
|
898
577
|
const data = (await response.json()) as { error?: string };
|
|
899
578
|
if (response.ok) {
|
|
900
579
|
comment.remove();
|
|
901
|
-
say("
|
|
580
|
+
say(t("comment.deleted"));
|
|
902
581
|
this.dispatchEvent(new CustomEvent("atmosphere:deleted", { detail: { uri }, bubbles: true }));
|
|
903
582
|
return;
|
|
904
583
|
}
|
|
@@ -907,7 +586,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
907
586
|
handleError(data.error ?? "error");
|
|
908
587
|
} catch {
|
|
909
588
|
panel.remove();
|
|
910
|
-
say(
|
|
589
|
+
say(message("error"));
|
|
911
590
|
}
|
|
912
591
|
};
|
|
913
592
|
|
|
@@ -920,15 +599,15 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
920
599
|
field("handle", me.did!);
|
|
921
600
|
field("permission", "delete");
|
|
922
601
|
field("returnTo", `${location.pathname}#atmo-delete=${encodeURIComponent(comment.dataset.atmosphereUri!)}`);
|
|
923
|
-
const go = Object.assign(make("button", "atmo-comment-action atmo-comment-action--danger", "
|
|
924
|
-
const cancel = Object.assign(make("button", "atmo-comment-action", "
|
|
602
|
+
const go = Object.assign(make("button", "atmo-comment-action atmo-comment-action--danger", t("comment.continue")), { type: "submit" });
|
|
603
|
+
const cancel = Object.assign(make("button", "atmo-comment-action", t("comment.cancel")), { type: "button" });
|
|
925
604
|
cancel.addEventListener("click", () => panel.remove());
|
|
926
605
|
form.append(go, cancel);
|
|
927
606
|
panel.replaceChildren(
|
|
928
607
|
make(
|
|
929
608
|
"span",
|
|
930
609
|
"atmo-comment-delete__text",
|
|
931
|
-
"
|
|
610
|
+
t("comment.deletePermission"),
|
|
932
611
|
),
|
|
933
612
|
form,
|
|
934
613
|
);
|
|
@@ -936,22 +615,22 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
936
615
|
};
|
|
937
616
|
|
|
938
617
|
const deleteButton = (comment: HTMLElement) => {
|
|
939
|
-
const button = Object.assign(make("button", "atmo-comment-action atmo-comment-action--delete", "
|
|
940
|
-
button.setAttribute("aria-label", "
|
|
618
|
+
const button = Object.assign(make("button", "atmo-comment-action atmo-comment-action--delete", t("comment.delete")), { type: "button" });
|
|
619
|
+
button.setAttribute("aria-label", t("comment.deleteLabel"));
|
|
941
620
|
button.addEventListener("click", () => {
|
|
942
621
|
const actions = button.parentElement!;
|
|
943
622
|
if (actions.nextElementSibling?.matches(".atmo-comment-delete")) return;
|
|
944
623
|
const panel = make("div", "atmo-comment-delete");
|
|
945
|
-
const yes = Object.assign(make("button", "atmo-comment-action atmo-comment-action--danger", "
|
|
946
|
-
const no = Object.assign(make("button", "atmo-comment-action", "
|
|
624
|
+
const yes = Object.assign(make("button", "atmo-comment-action atmo-comment-action--danger", t("comment.delete")), { type: "button" });
|
|
625
|
+
const no = Object.assign(make("button", "atmo-comment-action", t("comment.cancel")), { type: "button" });
|
|
947
626
|
yes.addEventListener("click", () => deleteComment(comment, panel));
|
|
948
627
|
no.addEventListener("click", () => {
|
|
949
628
|
panel.remove();
|
|
950
629
|
button.focus();
|
|
951
630
|
});
|
|
952
|
-
panel.append(make("span", "atmo-comment-delete__text", "
|
|
631
|
+
panel.append(make("span", "atmo-comment-delete__text", t("comment.deleteConfirm")), yes, no);
|
|
953
632
|
panel.setAttribute("role", "group");
|
|
954
|
-
panel.setAttribute("aria-label", "
|
|
633
|
+
panel.setAttribute("aria-label", t("comment.deleteLabel"));
|
|
955
634
|
actions.after(panel);
|
|
956
635
|
no.focus();
|
|
957
636
|
});
|
|
@@ -1033,7 +712,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1033
712
|
const url = new URL(location.href);
|
|
1034
713
|
const result = url.searchParams.get("atmosphere");
|
|
1035
714
|
if (result) {
|
|
1036
|
-
say(
|
|
715
|
+
say(MESSAGE_KEYS[result] ? message(result) : "");
|
|
1037
716
|
url.searchParams.delete("atmosphere");
|
|
1038
717
|
history.replaceState(history.state, "", url);
|
|
1039
718
|
}
|
|
@@ -1072,7 +751,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1072
751
|
setPressed(kind, !on);
|
|
1073
752
|
return handleError((await response.json()).error);
|
|
1074
753
|
}
|
|
1075
|
-
say(on ? (kind === "like" ? "
|
|
754
|
+
say(t(on ? (kind === "like" ? "status.liked" : "status.reposted") : kind === "like" ? "status.likeRemoved" : "status.repostRemoved"));
|
|
1076
755
|
showReaction(kind, on);
|
|
1077
756
|
this.dispatchEvent(
|
|
1078
757
|
new CustomEvent("atmosphere:reacted", {
|
|
@@ -1082,7 +761,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1082
761
|
);
|
|
1083
762
|
} catch {
|
|
1084
763
|
setPressed(kind, !on);
|
|
1085
|
-
say(
|
|
764
|
+
say(message("error"));
|
|
1086
765
|
}
|
|
1087
766
|
});
|
|
1088
767
|
}
|
|
@@ -1091,656 +770,10 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1091
770
|
await fetch(`${base}/logout`, { method: "POST", credentials: "same-origin" }).catch(() => undefined);
|
|
1092
771
|
void show(false);
|
|
1093
772
|
setMainMode("comment");
|
|
1094
|
-
say("
|
|
773
|
+
say(t("status.signedOut"));
|
|
1095
774
|
});
|
|
1096
775
|
}
|
|
1097
776
|
}
|
|
1098
777
|
|
|
1099
778
|
customElements.define("atmosphere-comments", AtmosphereComments);
|
|
1100
779
|
</script>
|
|
1101
|
-
|
|
1102
|
-
<style>
|
|
1103
|
-
.atmo {
|
|
1104
|
-
display: block;
|
|
1105
|
-
color: var(--atmo-text, currentColor);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/* Only the component's own parts wait for sign-in to be checked; the
|
|
1109
|
-
site's slot content shows straight away. */
|
|
1110
|
-
.atmo.is-loading > :is([data-view], [data-reactions], [data-replies]) {
|
|
1111
|
-
visibility: hidden;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
.atmo [hidden] {
|
|
1115
|
-
display: none !important;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
/* The Atmosphere Account logo inside the handle box, on the left, and at
|
|
1119
|
-
the start of the sign-in button. It is single-colour, so it is drawn in
|
|
1120
|
-
the muted text colour in the box and the button's text colour on it. */
|
|
1121
|
-
.atmo__handle {
|
|
1122
|
-
position: relative;
|
|
1123
|
-
display: flex;
|
|
1124
|
-
flex: 1 1 220px;
|
|
1125
|
-
min-width: 0;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
/* Matching accounts sit under the handle box, as wide as it. */
|
|
1129
|
-
.atmo__handle .atmo__suggestions {
|
|
1130
|
-
width: 100%;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
.atmo__handle .atmo__logo {
|
|
1134
|
-
position: absolute;
|
|
1135
|
-
top: 50%;
|
|
1136
|
-
left: 11px;
|
|
1137
|
-
width: 20px;
|
|
1138
|
-
height: 20px;
|
|
1139
|
-
transform: translateY(-50%);
|
|
1140
|
-
background-color: var(--atmo-muted, currentColor);
|
|
1141
|
-
mask: var(--atmo-logo) center / contain no-repeat;
|
|
1142
|
-
pointer-events: none;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
.atmo .atmo__handle input[type="text"] {
|
|
1146
|
-
padding-left: 40px;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
.atmo__button--sign-in {
|
|
1150
|
-
display: inline-flex;
|
|
1151
|
-
align-items: center;
|
|
1152
|
-
gap: 8px;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
.atmo__button--sign-in .atmo__logo {
|
|
1156
|
-
flex: none;
|
|
1157
|
-
width: 20px;
|
|
1158
|
-
height: 20px;
|
|
1159
|
-
background-color: currentColor;
|
|
1160
|
-
mask: var(--atmo-logo) center / contain no-repeat;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/* The comment box when it sits under a comment being replied to. */
|
|
1164
|
-
.atmo--inline {
|
|
1165
|
-
margin: 4px 0 6px;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
.atmo--inline .atmo__composer {
|
|
1169
|
-
margin-top: 10px;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
.atmo__elsewhere {
|
|
1173
|
-
max-width: 560px;
|
|
1174
|
-
margin: 14px 0 0;
|
|
1175
|
-
font-size: 0.9em;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
.atmo__elsewhere-link {
|
|
1179
|
-
font-weight: 700;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
.atmo__login,
|
|
1183
|
-
.atmo__composer {
|
|
1184
|
-
display: grid;
|
|
1185
|
-
gap: 8px;
|
|
1186
|
-
max-width: 560px;
|
|
1187
|
-
margin-top: 14px;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
.atmo label {
|
|
1191
|
-
font-weight: 700;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
.atmo__row {
|
|
1195
|
-
display: flex;
|
|
1196
|
-
flex-wrap: wrap;
|
|
1197
|
-
align-items: center;
|
|
1198
|
-
gap: 10px;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
.atmo__row--end {
|
|
1202
|
-
justify-content: flex-end;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
.atmo input[type="text"],
|
|
1206
|
-
.atmo textarea {
|
|
1207
|
-
flex: 1 1 220px;
|
|
1208
|
-
min-width: 0;
|
|
1209
|
-
padding: 10px 12px;
|
|
1210
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1211
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1212
|
-
background: var(--atmo-field-bg, transparent);
|
|
1213
|
-
color: inherit;
|
|
1214
|
-
font: inherit;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
.atmo textarea {
|
|
1218
|
-
resize: vertical;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
.atmo__button {
|
|
1222
|
-
padding: 10px 16px;
|
|
1223
|
-
border: 0;
|
|
1224
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1225
|
-
background: var(--atmo-accent, #1185fe);
|
|
1226
|
-
color: var(--atmo-on-accent, #fff);
|
|
1227
|
-
font: inherit;
|
|
1228
|
-
font-weight: 700;
|
|
1229
|
-
cursor: pointer;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
.atmo__button:disabled {
|
|
1233
|
-
opacity: 0.6;
|
|
1234
|
-
cursor: progress;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
.atmo__link-button {
|
|
1238
|
-
padding: 0;
|
|
1239
|
-
border: 0;
|
|
1240
|
-
background: none;
|
|
1241
|
-
color: var(--atmo-muted, inherit);
|
|
1242
|
-
font: inherit;
|
|
1243
|
-
text-decoration: underline;
|
|
1244
|
-
cursor: pointer;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
/* Like, Repost and Quote: outlined pills that fill in once done. */
|
|
1248
|
-
.atmo__actions {
|
|
1249
|
-
display: flex;
|
|
1250
|
-
flex-wrap: wrap;
|
|
1251
|
-
gap: 8px;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
.atmo__action {
|
|
1255
|
-
display: inline-flex;
|
|
1256
|
-
align-items: center;
|
|
1257
|
-
gap: 6px;
|
|
1258
|
-
padding: 7px 14px 7px 10px;
|
|
1259
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1260
|
-
border-radius: 999px;
|
|
1261
|
-
background: none;
|
|
1262
|
-
color: inherit;
|
|
1263
|
-
font: inherit;
|
|
1264
|
-
font-weight: 600;
|
|
1265
|
-
cursor: pointer;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
.atmo__action:hover,
|
|
1269
|
-
.atmo__action:focus-visible {
|
|
1270
|
-
border-color: var(--atmo-accent, #1185fe);
|
|
1271
|
-
color: var(--atmo-accent, #1185fe);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
.atmo__action svg {
|
|
1275
|
-
width: 18px;
|
|
1276
|
-
height: 18px;
|
|
1277
|
-
fill: none;
|
|
1278
|
-
stroke: currentColor;
|
|
1279
|
-
stroke-width: 1.8;
|
|
1280
|
-
stroke-linecap: round;
|
|
1281
|
-
stroke-linejoin: round;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
.atmo__action[aria-pressed="true"] {
|
|
1285
|
-
border-color: var(--atmo-accent, #1185fe);
|
|
1286
|
-
color: var(--atmo-accent, #1185fe);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
.atmo__action[data-react="like"][aria-pressed="true"] svg {
|
|
1290
|
-
fill: currentColor;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
.atmo__action [data-label-on],
|
|
1294
|
-
.atmo__action[aria-pressed="true"] [data-label-off] {
|
|
1295
|
-
display: none;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
.atmo__action[aria-pressed="true"] [data-label-on] {
|
|
1299
|
-
display: inline;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
.atmo__context {
|
|
1303
|
-
display: flex;
|
|
1304
|
-
flex-wrap: wrap;
|
|
1305
|
-
gap: 10px;
|
|
1306
|
-
margin: 0;
|
|
1307
|
-
color: var(--atmo-muted, inherit);
|
|
1308
|
-
font-size: 0.9em;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
.atmo__hint,
|
|
1312
|
-
.atmo__count,
|
|
1313
|
-
.atmo__status {
|
|
1314
|
-
margin: 0;
|
|
1315
|
-
color: var(--atmo-muted, inherit);
|
|
1316
|
-
font-size: 0.9em;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
.atmo__count {
|
|
1320
|
-
margin-right: auto;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
.atmo__count.is-over {
|
|
1324
|
-
color: var(--atmo-error, #d33);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
.atmo__status:not(:empty) {
|
|
1328
|
-
margin-top: 10px;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
.atmo__credit {
|
|
1332
|
-
margin: 14px 0 0;
|
|
1333
|
-
color: var(--atmo-muted, inherit);
|
|
1334
|
-
font-size: 0.75em;
|
|
1335
|
-
opacity: 0.8;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
.atmo__credit a {
|
|
1339
|
-
color: inherit;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
/* @mention suggestions, under the text box. */
|
|
1343
|
-
.atmo__field {
|
|
1344
|
-
position: relative;
|
|
1345
|
-
display: grid;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
.atmo__suggestions {
|
|
1349
|
-
position: absolute;
|
|
1350
|
-
z-index: 5;
|
|
1351
|
-
top: calc(100% + 4px);
|
|
1352
|
-
left: 0;
|
|
1353
|
-
width: min(100%, 360px);
|
|
1354
|
-
margin: 0;
|
|
1355
|
-
padding: 4px;
|
|
1356
|
-
list-style: none;
|
|
1357
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1358
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1359
|
-
background: var(--atmo-menu-bg, Canvas);
|
|
1360
|
-
box-shadow: 0 12px 30px rgb(0 0 0 / 0.25);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
.atmo__suggestions :global(li) {
|
|
1364
|
-
display: flex;
|
|
1365
|
-
align-items: center;
|
|
1366
|
-
gap: 8px;
|
|
1367
|
-
padding: 7px 9px;
|
|
1368
|
-
border-radius: calc(var(--atmo-radius, 10px) - 4px);
|
|
1369
|
-
cursor: pointer;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
.atmo__suggestions :global(li[aria-selected="true"]),
|
|
1373
|
-
.atmo__suggestions :global(li:hover) {
|
|
1374
|
-
background: color-mix(in srgb, var(--atmo-accent, #1185fe) 14%, transparent);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
.atmo__suggestions :global(li span:last-child) {
|
|
1378
|
-
overflow: hidden;
|
|
1379
|
-
color: var(--atmo-muted, inherit);
|
|
1380
|
-
text-overflow: ellipsis;
|
|
1381
|
-
white-space: nowrap;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
.atmo__suggestions :global(.atmo__avatar) {
|
|
1385
|
-
display: grid;
|
|
1386
|
-
flex: none;
|
|
1387
|
-
place-items: center;
|
|
1388
|
-
width: 26px;
|
|
1389
|
-
height: 26px;
|
|
1390
|
-
overflow: hidden;
|
|
1391
|
-
border-radius: 50%;
|
|
1392
|
-
background: var(--atmo-border, #8886);
|
|
1393
|
-
font-size: 11px;
|
|
1394
|
-
font-weight: 700;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
.atmo__suggestions :global(.atmo__avatar img) {
|
|
1398
|
-
width: 100%;
|
|
1399
|
-
height: 100%;
|
|
1400
|
-
object-fit: cover;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
/* The Quote preview: a post card with the quoted post nested inside. */
|
|
1404
|
-
.atmo__preview {
|
|
1405
|
-
display: grid;
|
|
1406
|
-
gap: 6px;
|
|
1407
|
-
margin: 4px 0 0;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
.atmo__preview figcaption {
|
|
1411
|
-
color: var(--atmo-muted, inherit);
|
|
1412
|
-
font-size: 0.85em;
|
|
1413
|
-
font-weight: 700;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
.atmo__preview-post,
|
|
1417
|
-
.atmo__quoted {
|
|
1418
|
-
padding: 12px 14px;
|
|
1419
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1420
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
/* As on Bluesky: the avatar beside the post, the post's parts stacked. */
|
|
1424
|
-
.atmo__preview-post {
|
|
1425
|
-
display: grid;
|
|
1426
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
1427
|
-
gap: 12px;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
.atmo__preview-body,
|
|
1431
|
-
.atmo__quoted {
|
|
1432
|
-
display: grid;
|
|
1433
|
-
gap: 6px;
|
|
1434
|
-
min-width: 0;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
.atmo__avatar {
|
|
1438
|
-
display: grid;
|
|
1439
|
-
place-items: center;
|
|
1440
|
-
width: 42px;
|
|
1441
|
-
height: 42px;
|
|
1442
|
-
overflow: hidden;
|
|
1443
|
-
border-radius: 50%;
|
|
1444
|
-
background: var(--atmo-border, #8886);
|
|
1445
|
-
color: var(--atmo-text, inherit);
|
|
1446
|
-
font-weight: 700;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
/* "Comment as @handle", with the reader's avatar as a reminder of which
|
|
1450
|
-
account they're signed in with. */
|
|
1451
|
-
.atmo__as {
|
|
1452
|
-
display: flex;
|
|
1453
|
-
align-items: center;
|
|
1454
|
-
gap: 8px;
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
.atmo__avatar--me {
|
|
1458
|
-
width: 28px;
|
|
1459
|
-
height: 28px;
|
|
1460
|
-
font-size: 12px;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
.atmo__avatar--small {
|
|
1464
|
-
display: inline-grid;
|
|
1465
|
-
width: 18px;
|
|
1466
|
-
height: 18px;
|
|
1467
|
-
margin-right: 4px;
|
|
1468
|
-
font-size: 10px;
|
|
1469
|
-
vertical-align: -4px;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
/* Added by the script, so outside Astro's scoped styles. */
|
|
1473
|
-
.atmo__avatar :global(img) {
|
|
1474
|
-
width: 100%;
|
|
1475
|
-
height: 100%;
|
|
1476
|
-
object-fit: cover;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
.atmo__preview-post p,
|
|
1480
|
-
.atmo__card span {
|
|
1481
|
-
margin: 0;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
.atmo__preview-author span,
|
|
1485
|
-
.atmo__card-domain {
|
|
1486
|
-
color: var(--atmo-muted, inherit);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
.atmo__preview-text {
|
|
1490
|
-
white-space: pre-wrap;
|
|
1491
|
-
overflow-wrap: anywhere;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
.atmo__preview-text.is-placeholder {
|
|
1495
|
-
color: var(--atmo-muted, inherit);
|
|
1496
|
-
font-style: italic;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
.atmo__quoted {
|
|
1500
|
-
font-size: 0.92em;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
.atmo__card {
|
|
1504
|
-
overflow: hidden;
|
|
1505
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1506
|
-
border-radius: var(--atmo-radius, 10px);
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
.atmo__card img {
|
|
1510
|
-
display: block;
|
|
1511
|
-
width: 100%;
|
|
1512
|
-
aspect-ratio: 1.91 / 1;
|
|
1513
|
-
object-fit: cover;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
.atmo__card > div {
|
|
1517
|
-
display: grid;
|
|
1518
|
-
gap: 2px;
|
|
1519
|
-
padding: 10px 12px;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
.atmo__card [data-card-description] {
|
|
1523
|
-
display: -webkit-box;
|
|
1524
|
-
overflow: hidden;
|
|
1525
|
-
-webkit-box-orient: vertical;
|
|
1526
|
-
-webkit-line-clamp: 2;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
.atmo__card-domain {
|
|
1530
|
-
font-size: 0.85em;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
/* The likes and reposts, and the comments. These are drawn by the script,
|
|
1534
|
-
and wrapped in :where() so they carry no weight: any rule a site writes
|
|
1535
|
-
for these classes wins. */
|
|
1536
|
-
:global(:where(.atmo-reactions)) {
|
|
1537
|
-
display: flex;
|
|
1538
|
-
flex-wrap: wrap;
|
|
1539
|
-
gap: 8px 32px;
|
|
1540
|
-
margin: 0 0 18px;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
:global(:where(.atmo-reactions__count)) {
|
|
1544
|
-
margin: 0 0 8px;
|
|
1545
|
-
font-weight: 700;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
:global(:where(.atmo-facepile)) {
|
|
1549
|
-
display: flex;
|
|
1550
|
-
flex-wrap: wrap;
|
|
1551
|
-
margin: 0;
|
|
1552
|
-
padding: 0 0 0 8px;
|
|
1553
|
-
list-style: none;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
:global(:where(.atmo-facepile li)) {
|
|
1557
|
-
margin-left: -8px;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
:global(:where(.atmo-facepile a)) {
|
|
1561
|
-
display: block;
|
|
1562
|
-
border-radius: 50%;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
:global(:where(.atmo-facepile__avatar, .atmo-facepile__more)) {
|
|
1566
|
-
display: grid;
|
|
1567
|
-
place-items: center;
|
|
1568
|
-
width: 36px;
|
|
1569
|
-
height: 36px;
|
|
1570
|
-
border: 2px solid var(--atmo-ring, Canvas);
|
|
1571
|
-
border-radius: 50%;
|
|
1572
|
-
object-fit: cover;
|
|
1573
|
-
background: var(--atmo-field-bg, #8882);
|
|
1574
|
-
font-size: 12px;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
:global(:where(.atmo-facepile__more)) {
|
|
1578
|
-
width: auto;
|
|
1579
|
-
min-width: 36px;
|
|
1580
|
-
padding: 0 8px;
|
|
1581
|
-
border-radius: 18px;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
:global(:where(.atmo-avatar-initial)) {
|
|
1585
|
-
display: inline-grid;
|
|
1586
|
-
place-items: center;
|
|
1587
|
-
color: #fff;
|
|
1588
|
-
font-weight: 800;
|
|
1589
|
-
background: var(--atmo-accent, #1185fe);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
:global(:where(.atmo-comments)) {
|
|
1593
|
-
display: grid;
|
|
1594
|
-
gap: 12px;
|
|
1595
|
-
margin: 24px 0 0;
|
|
1596
|
-
padding: 0;
|
|
1597
|
-
list-style: none;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
:global(:where(.atmo-comments:empty)) {
|
|
1601
|
-
display: none;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
:global(:where(.atmo-comment article)) {
|
|
1605
|
-
padding: 16px 18px;
|
|
1606
|
-
border: 1px solid var(--atmo-border, #8886);
|
|
1607
|
-
border-radius: 16px;
|
|
1608
|
-
background: var(--atmo-card-bg, transparent);
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
:global(:where(.atmo-comment--author > article)) {
|
|
1612
|
-
border-color: var(--atmo-accent, #1185fe);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
/* Replies step in under the comment they answer (up to three levels). */
|
|
1616
|
-
:global(:where(.atmo-comment--nested)) {
|
|
1617
|
-
margin-left: calc((var(--depth, 1) - 1) * 24px);
|
|
1618
|
-
padding-left: 22px;
|
|
1619
|
-
border-left: 2px solid var(--atmo-border, #8886);
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
:global(:where(.atmo-comment__header)) {
|
|
1623
|
-
display: flex;
|
|
1624
|
-
align-items: center;
|
|
1625
|
-
gap: 12px;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
:global(:where(.atmo-comment__avatar)) {
|
|
1629
|
-
flex: none;
|
|
1630
|
-
width: 40px;
|
|
1631
|
-
height: 40px;
|
|
1632
|
-
border-radius: 50%;
|
|
1633
|
-
object-fit: cover;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
:global(:where(.atmo-comment__who)) {
|
|
1637
|
-
display: flex;
|
|
1638
|
-
flex-wrap: wrap;
|
|
1639
|
-
align-items: center;
|
|
1640
|
-
gap: 8px;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
:global(:where(.atmo-comment__author)) {
|
|
1644
|
-
color: inherit;
|
|
1645
|
-
font-weight: 750;
|
|
1646
|
-
text-decoration: none;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
:global(:where(.atmo-comment__badge)) {
|
|
1650
|
-
padding: 2px 7px;
|
|
1651
|
-
border: 1px solid var(--atmo-accent, #1185fe);
|
|
1652
|
-
border-radius: 999px;
|
|
1653
|
-
color: var(--atmo-accent, #1185fe);
|
|
1654
|
-
font-size: 11px;
|
|
1655
|
-
font-weight: 800;
|
|
1656
|
-
letter-spacing: 0.04em;
|
|
1657
|
-
text-transform: uppercase;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
:global(:where(.atmo-comment__date)) {
|
|
1661
|
-
color: var(--atmo-muted, inherit);
|
|
1662
|
-
font-size: 13px;
|
|
1663
|
-
text-decoration: none;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
:global(:where(.atmo-comment__body)) {
|
|
1667
|
-
margin-top: 10px;
|
|
1668
|
-
line-height: 1.65;
|
|
1669
|
-
overflow-wrap: anywhere;
|
|
1670
|
-
white-space: pre-line;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
:global(:where(.atmo-comment__body p)) {
|
|
1674
|
-
margin: 0;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
/* Reply, Like and Repost under each comment (outside this element, so
|
|
1678
|
-
unscoped). */
|
|
1679
|
-
:global(.atmo-comment-actions) {
|
|
1680
|
-
display: flex;
|
|
1681
|
-
flex-wrap: wrap;
|
|
1682
|
-
gap: 18px;
|
|
1683
|
-
margin-top: 8px;
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
:global(.atmo-comment-delete) {
|
|
1687
|
-
display: flex;
|
|
1688
|
-
flex-wrap: wrap;
|
|
1689
|
-
align-items: center;
|
|
1690
|
-
gap: 6px 12px;
|
|
1691
|
-
margin-top: 8px;
|
|
1692
|
-
font-size: 0.9em;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
:global(.atmo-comment-delete form) {
|
|
1696
|
-
display: contents;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
:global(.atmo-comment-delete__text) {
|
|
1700
|
-
color: var(--atmo-muted, inherit);
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
:global(.atmo-comment-action) {
|
|
1705
|
-
display: inline-flex;
|
|
1706
|
-
align-items: center;
|
|
1707
|
-
gap: 5px;
|
|
1708
|
-
padding: 0;
|
|
1709
|
-
border: 0;
|
|
1710
|
-
background: none;
|
|
1711
|
-
color: var(--atmo-muted, inherit);
|
|
1712
|
-
font: inherit;
|
|
1713
|
-
font-size: 0.85em;
|
|
1714
|
-
font-weight: 600;
|
|
1715
|
-
cursor: pointer;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
:global(.atmo-comment-action--delete) {
|
|
1719
|
-
margin-left: auto;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
:global(.atmo-comment-action--danger) {
|
|
1723
|
-
color: var(--atmo-danger, #c0392b);
|
|
1724
|
-
font-weight: 700;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
:global(.atmo-comment-action:hover),
|
|
1728
|
-
:global(.atmo-comment-action:focus-visible),
|
|
1729
|
-
:global(.atmo-comment-action[aria-pressed="true"]) {
|
|
1730
|
-
color: var(--atmo-accent, #1185fe);
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
:global(.atmo-comment-action svg) {
|
|
1734
|
-
width: 16px;
|
|
1735
|
-
height: 16px;
|
|
1736
|
-
fill: none;
|
|
1737
|
-
stroke: currentColor;
|
|
1738
|
-
stroke-width: 1.8;
|
|
1739
|
-
stroke-linecap: round;
|
|
1740
|
-
stroke-linejoin: round;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
:global(.atmo-comment-action[data-kind="like"][aria-pressed="true"] svg) {
|
|
1744
|
-
fill: currentColor;
|
|
1745
|
-
}
|
|
1746
|
-
</style>
|