astro-atmosphere-comments 0.1.1 → 0.3.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 +18 -0
- package/README.md +37 -13
- package/components/AtmosphereComments.astro +406 -127
- package/index.d.ts +11 -0
- package/index.mjs +18 -5
- package/lib/i18n.ts +32 -0
- package/lib/oauth.ts +18 -7
- package/lib/reader.ts +18 -3
- package/lib/settings.ts +2 -0
- package/lib/translate.ts +36 -0
- package/locales/de.json +75 -0
- package/locales/en.json +75 -0
- package/locales/es.json +78 -0
- package/locales/fr.json +78 -0
- package/locales/it.json +78 -0
- package/locales/ja.json +72 -0
- package/locales/nl.json +75 -0
- package/locales/pt.json +78 -0
- package/package.json +3 -2
- package/routes/delete.ts +41 -0
- package/routes/login.ts +4 -2
- package/routes/typeahead.ts +48 -18
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
// The comment box under a post:
|
|
3
|
-
// once signed in, Like, Repost and Quote buttons and a box to write a
|
|
2
|
+
// The comment box under a post: a form to sign in with an Atmosphere account
|
|
3
|
+
// (Bluesky or any other), then, once signed in, Like, Repost and Quote buttons and a box to write a
|
|
4
4
|
// comment (a reply to the post's announcement, or to one of its comments).
|
|
5
5
|
//
|
|
6
6
|
// Props:
|
|
7
7
|
// post the announcement's AT URI (at://did/app.bsky.feed.post/rkey).
|
|
8
|
-
// label the
|
|
9
|
-
// (default "
|
|
8
|
+
// label the sign-in form's heading
|
|
9
|
+
// (default "Sign in to leave a comment, like or repost").
|
|
10
|
+
// blueskyLink "Rather not sign in here? Reply on Bluesky instead →" under the
|
|
11
|
+
// sign-in form (default true; false hides it).
|
|
10
12
|
// credit a small "Powered by Atmosphere Comments for Astro" link under the
|
|
11
13
|
// comments (default true; false hides it).
|
|
12
14
|
//
|
|
13
15
|
// Quote shows a preview of the reader's post with the announcement quoted
|
|
14
16
|
// inside, avatars and link-card picture included (passed on by this site).
|
|
15
17
|
//
|
|
16
|
-
// Typing @ and the start of a handle suggests accounts to mention
|
|
18
|
+
// Typing @ and the start of a handle suggests accounts to mention, and typing
|
|
19
|
+
// in the sign-in box suggests matching accounts to sign in with.
|
|
17
20
|
//
|
|
18
21
|
// Comments: under every element on the page with a `data-atmosphere-uri`
|
|
19
22
|
// attribute (a comment's AT URI) the component adds Reply, Like and Repost.
|
|
@@ -30,6 +33,8 @@
|
|
|
30
33
|
// can show it straight away.
|
|
31
34
|
import logo from "../assets/union.svg?url";
|
|
32
35
|
import settings from "../lib/settings";
|
|
36
|
+
import { messagesFor, pickLocale, translate, translateHtml } from "../lib/i18n";
|
|
37
|
+
import { escapeHtml } from "../lib/translate";
|
|
33
38
|
|
|
34
39
|
interface Props {
|
|
35
40
|
/**
|
|
@@ -38,80 +43,127 @@ interface Props {
|
|
|
38
43
|
* is used to find it.
|
|
39
44
|
*/
|
|
40
45
|
post?: string;
|
|
46
|
+
/** The sign-in form's heading. */
|
|
41
47
|
label?: string;
|
|
42
48
|
/** The badge on comments by the site's own accounts. */
|
|
43
49
|
authorLabel?: string;
|
|
44
50
|
/** A small "Powered by Atmosphere Comments for Astro" link under the comments. Default true. */
|
|
45
51
|
credit?: boolean;
|
|
52
|
+
/** "Rather not sign in here? Reply on Bluesky instead →" under the sign-in form. Default true. */
|
|
53
|
+
blueskyLink?: boolean;
|
|
54
|
+
/** The language of the wording, e.g. "fr". Overrides the integration's `locale`. */
|
|
55
|
+
locale?: string;
|
|
56
|
+
/** Your own wording for any line, by its key in locales/en.json. */
|
|
57
|
+
messages?: Record<string, string>;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
const { post = "", label
|
|
60
|
+
const { post = "", label, authorLabel, credit = true, blueskyLink = true } = Astro.props;
|
|
61
|
+
// The wording: the language asked for (or Astro's i18n locale for the page),
|
|
62
|
+
// with the site's own lines on top. The script gets the same, as JSON.
|
|
63
|
+
const lang = pickLocale(Astro.props.locale, settings.locale, Astro.currentLocale);
|
|
64
|
+
const messages = messagesFor(lang, {
|
|
65
|
+
...settings.messages,
|
|
66
|
+
...Astro.props.messages,
|
|
67
|
+
...(label && { "signIn.heading": label }),
|
|
68
|
+
...(authorLabel && { "comment.author": authorLabel }),
|
|
69
|
+
});
|
|
70
|
+
const t = (key: string, vars?: Record<string, string | number>) => translate(messages, lang, key, vars);
|
|
71
|
+
const th = (key: string, html: Record<string, string>) => translateHtml(messages, lang, key, html);
|
|
72
|
+
const link = (href: string | undefined, text: string, attributes = "") =>
|
|
73
|
+
`<a${href ? ` href="${escapeHtml(href)}"` : ""}${attributes}>${escapeHtml(text)}</a>`;
|
|
74
|
+
// The announcement on bsky.app, when the page is given it (otherwise the
|
|
75
|
+
// script fills it in once the thread has loaded).
|
|
76
|
+
const atUri = post.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/);
|
|
77
|
+
const blueskyUrl = atUri
|
|
78
|
+
? `https://bsky.app/profile/${atUri[1]}/post/${atUri[2]}`
|
|
79
|
+
: /^https:\/\/bsky\.app\/profile\/[^/]+\/post\/[^/]+$/.test(post)
|
|
80
|
+
? post
|
|
81
|
+
: "";
|
|
49
82
|
const CREDIT_URL = "https://saltedlolly.com/atmosphere-comments-for-astro/";
|
|
50
83
|
const { base } = settings;
|
|
51
84
|
const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
52
85
|
---
|
|
53
86
|
|
|
54
|
-
<atmosphere-comments class="atmo" id="atmosphere-comments" data-post={post} data-base={base} data-
|
|
87
|
+
<atmosphere-comments class="atmo" id="atmosphere-comments" data-post={post} data-base={base} data-locale={lang} data-messages={JSON.stringify(messages)}>
|
|
55
88
|
<div class="atmo-reactions" data-reactions data-pagefind-ignore hidden></div>
|
|
56
89
|
|
|
57
90
|
<div class="atmo__signed-out" data-view="signed-out" data-pagefind-ignore>
|
|
58
|
-
<button type="button" class="atmo__badge-button" data-open-login aria-expanded="false" aria-controls="atmo-login" hidden>
|
|
59
|
-
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
60
|
-
<span class="atmo__badge-text">
|
|
61
|
-
<span class="atmo__badge-small">{label}</span>
|
|
62
|
-
<span class="atmo__badge-big">Atmosphere Account</span>
|
|
63
|
-
</span>
|
|
64
|
-
</button>
|
|
65
91
|
<form class="atmo__login" id="atmo-login" method="post" action={`${base}/login`} data-login-form>
|
|
66
|
-
<label for="atmo-handle">
|
|
67
|
-
<p
|
|
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
|
+
/>
|
|
68
100
|
<div class="atmo__row">
|
|
69
|
-
<
|
|
101
|
+
<span class="atmo__handle">
|
|
102
|
+
<span class="atmo__logo" style={`--atmo-logo: url("${logo}")`} aria-hidden="true"></span>
|
|
103
|
+
<input
|
|
70
104
|
id="atmo-handle"
|
|
71
105
|
name="handle"
|
|
72
106
|
type="text"
|
|
73
107
|
inputmode="url"
|
|
74
|
-
|
|
108
|
+
data-1p-ignore
|
|
109
|
+
data-lpignore="true"
|
|
110
|
+
data-bwignore
|
|
111
|
+
data-form-type="other"
|
|
75
112
|
autocapitalize="none"
|
|
76
113
|
spellcheck="false"
|
|
77
114
|
placeholder="alice.bsky.social"
|
|
78
115
|
required
|
|
79
|
-
|
|
116
|
+
/>
|
|
117
|
+
<ul class="atmo__suggestions" id="atmo-handle-suggestions" role="listbox" aria-label={t("signIn.matchingAccounts")} data-handle-suggestions hidden></ul>
|
|
118
|
+
</span>
|
|
80
119
|
<input type="hidden" name="returnTo" value={returnTo} />
|
|
81
|
-
<button type="submit" class="atmo__button">
|
|
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>
|
|
82
124
|
</div>
|
|
83
|
-
<p
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</p>
|
|
125
|
+
<p
|
|
126
|
+
class="atmo__hint"
|
|
127
|
+
set:html={th("signIn.privacy", { createAccount: link("https://atmosphereaccount.com", t("signIn.createAccount")) })}
|
|
128
|
+
/>
|
|
88
129
|
</form>
|
|
130
|
+
{blueskyLink && (
|
|
131
|
+
<p
|
|
132
|
+
class="atmo__elsewhere"
|
|
133
|
+
data-elsewhere
|
|
134
|
+
hidden={!blueskyUrl}
|
|
135
|
+
set:html={th("elsewhere.text", {
|
|
136
|
+
// u-syndication: microformats readers learn this is the post's copy on Bluesky.
|
|
137
|
+
link: link(blueskyUrl || undefined, t("elsewhere.link"), ' class="atmo__elsewhere-link u-syndication" data-elsewhere-link'),
|
|
138
|
+
})}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
89
141
|
</div>
|
|
90
142
|
|
|
91
143
|
<div class="atmo__signed-in" data-view="signed-in" data-pagefind-ignore hidden>
|
|
92
|
-
<div class="atmo__actions" role="group" aria-label="
|
|
144
|
+
<div class="atmo__actions" role="group" aria-label={t("actions.label")}>
|
|
93
145
|
<button type="button" class="atmo__action" data-react="like" aria-pressed="false">
|
|
94
146
|
<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>
|
|
95
|
-
<span data-label-off>
|
|
147
|
+
<span data-label-off>{t("actions.like")}</span><span data-label-on>{t("actions.liked")}</span>
|
|
96
148
|
</button>
|
|
97
149
|
<button type="button" class="atmo__action" data-react="repost" aria-pressed="false">
|
|
98
150
|
<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>
|
|
99
|
-
<span data-label-off>
|
|
151
|
+
<span data-label-off>{t("actions.repost")}</span><span data-label-on>{t("actions.reposted")}</span>
|
|
100
152
|
</button>
|
|
101
153
|
<button type="button" class="atmo__action" data-quote>
|
|
102
154
|
<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>
|
|
103
|
-
<span>
|
|
155
|
+
<span>{t("actions.quote")}</span>
|
|
104
156
|
</button>
|
|
105
157
|
</div>
|
|
106
158
|
|
|
107
159
|
<form class="atmo__composer" data-composer>
|
|
108
160
|
<p class="atmo__context" data-context hidden>
|
|
109
161
|
<span data-context-text></span>
|
|
110
|
-
<button type="button" class="atmo__link-button" data-cancel-context>
|
|
162
|
+
<button type="button" class="atmo__link-button" data-cancel-context>{t("composer.cancel")}</button>
|
|
111
163
|
</p>
|
|
112
164
|
<label for="atmo-text" class="atmo__as">
|
|
113
165
|
<span class="atmo__avatar atmo__avatar--me" data-me-avatar aria-hidden="true"></span>
|
|
114
|
-
<span
|
|
166
|
+
<span set:html={th("composer.commentAs", { verb: `<span data-verb>${escapeHtml(t("composer.comment"))}</span>`, handle: "<strong data-handle></strong>" })} />
|
|
115
167
|
</label>
|
|
116
168
|
<div class="atmo__field">
|
|
117
169
|
<textarea
|
|
@@ -122,14 +174,14 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
122
174
|
aria-autocomplete="list"
|
|
123
175
|
aria-controls="atmo-suggestions"
|
|
124
176
|
aria-expanded="false"></textarea>
|
|
125
|
-
<ul class="atmo__suggestions" id="atmo-suggestions" role="listbox" aria-label="
|
|
177
|
+
<ul class="atmo__suggestions" id="atmo-suggestions" role="listbox" aria-label={t("composer.mentions")} data-suggestions hidden></ul>
|
|
126
178
|
</div>
|
|
127
179
|
<figure class="atmo__preview" data-preview hidden>
|
|
128
|
-
<figcaption>
|
|
180
|
+
<figcaption>{t("composer.previewCaption")}</figcaption>
|
|
129
181
|
<div class="atmo__preview-post">
|
|
130
182
|
<span class="atmo__avatar" data-preview-avatar aria-hidden="true"></span>
|
|
131
183
|
<div class="atmo__preview-body">
|
|
132
|
-
<p class="atmo__preview-author"><strong data-preview-name></strong> <span data-preview-handle></span> <span>· now</span></p>
|
|
184
|
+
<p class="atmo__preview-author"><strong data-preview-name></strong> <span data-preview-handle></span> <span>· {t("composer.now")}</span></p>
|
|
133
185
|
<p class="atmo__preview-text" data-preview-text></p>
|
|
134
186
|
<div class="atmo__quoted">
|
|
135
187
|
<p class="atmo__preview-author">
|
|
@@ -151,8 +203,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
151
203
|
</figure>
|
|
152
204
|
<div class="atmo__row atmo__row--end">
|
|
153
205
|
<span class="atmo__count" data-count aria-live="polite"></span>
|
|
154
|
-
<button type="button" class="atmo__link-button" data-sign-out>
|
|
155
|
-
<button type="submit" class="atmo__button" data-submit>
|
|
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>
|
|
156
208
|
</div>
|
|
157
209
|
</form>
|
|
158
210
|
</div>
|
|
@@ -168,27 +220,31 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
168
220
|
<slot />
|
|
169
221
|
|
|
170
222
|
{credit && (
|
|
171
|
-
<p class="atmo__credit" data-credit data-pagefind-ignore
|
|
172
|
-
Powered by <a href={CREDIT_URL}>Atmosphere Comments for Astro</a>
|
|
173
|
-
</p>
|
|
223
|
+
<p class="atmo__credit" data-credit data-pagefind-ignore set:html={th("credit", { link: link(CREDIT_URL, "Atmosphere Comments for Astro") })} />
|
|
174
224
|
)}
|
|
175
225
|
</atmosphere-comments>
|
|
176
226
|
|
|
177
227
|
<script>
|
|
228
|
+
import { translate } from "../lib/translate";
|
|
229
|
+
|
|
178
230
|
const MAX = 300;
|
|
179
231
|
const graphemes = new Intl.Segmenter();
|
|
180
232
|
const count = (text: string) => [...graphemes.segment(text.trim())].length;
|
|
181
233
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
|
|
234
|
+
// The error and status codes the routes and the sign-in round trip send,
|
|
235
|
+
// and the wording for each (in locales/*.json).
|
|
236
|
+
const MESSAGE_KEYS: Record<string, string> = {
|
|
237
|
+
"signed-in": "messages.signedIn",
|
|
238
|
+
"sign-in-failed": "messages.signInFailed",
|
|
239
|
+
"account-not-found": "messages.accountNotFound",
|
|
240
|
+
"invalid-handle": "messages.invalidHandle",
|
|
241
|
+
"signed-out": "messages.signedOut",
|
|
242
|
+
"needs-permission": "messages.needsPermission",
|
|
243
|
+
"not-allowed": "messages.notAllowed",
|
|
244
|
+
"new-permissions": "messages.newPermissions",
|
|
245
|
+
"too-long": "messages.tooLong",
|
|
246
|
+
"too-many": "messages.tooMany",
|
|
247
|
+
error: "messages.error",
|
|
192
248
|
};
|
|
193
249
|
|
|
194
250
|
type Posted = { uri: string; url: string; quote: boolean };
|
|
@@ -197,6 +253,11 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
197
253
|
class AtmosphereComments extends HTMLElement {
|
|
198
254
|
connectedCallback() {
|
|
199
255
|
const base = this.dataset.base ?? "/atmosphere";
|
|
256
|
+
// The wording, in the page's language, from the server.
|
|
257
|
+
const lang = this.dataset.locale || "en";
|
|
258
|
+
const wording = JSON.parse(this.dataset.messages || "{}") as Record<string, string>;
|
|
259
|
+
const t = (key: string, vars?: Record<string, string | number>) => translate(wording, lang, key, vars);
|
|
260
|
+
const message = (code: string) => t(MESSAGE_KEYS[code] ?? MESSAGE_KEYS.error, { max: MAX });
|
|
200
261
|
// The announcement's AT URI. A bsky.app address or the page's
|
|
201
262
|
// Standard.site record is turned into one when the thread loads.
|
|
202
263
|
let post = this.dataset.post?.startsWith("at://") ? this.dataset.post : "";
|
|
@@ -206,7 +267,6 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
206
267
|
const composer = $<HTMLFormElement>("[data-composer]");
|
|
207
268
|
// A clean copy of the comment box, for reply boxes under comments.
|
|
208
269
|
const template = composer.cloneNode(true) as HTMLFormElement;
|
|
209
|
-
const loginButton = $<HTMLButtonElement>("[data-open-login]");
|
|
210
270
|
const loginForm = $<HTMLFormElement>("[data-login-form]");
|
|
211
271
|
const status = $<HTMLElement>("[data-status]");
|
|
212
272
|
let isSignedIn = false;
|
|
@@ -245,13 +305,13 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
245
305
|
likeCount: number;
|
|
246
306
|
repostCount: number;
|
|
247
307
|
};
|
|
248
|
-
type Thread = { uri: string; likeCount: number; repostCount: number; likes: Person[]; reposts: Person[]; replies: Reply[] };
|
|
308
|
+
type Thread = { uri: string; url: string; likeCount: number; repostCount: number; likes: Person[]; reposts: Person[]; replies: Reply[] };
|
|
249
309
|
let thread: Thread | null = null;
|
|
250
310
|
const reactionsBox = $<HTMLElement>("[data-reactions]");
|
|
251
311
|
const repliesList = $<HTMLOListElement>("[data-replies]");
|
|
252
|
-
const authorLabel =
|
|
312
|
+
const authorLabel = t("comment.author");
|
|
253
313
|
const nameOf = (person: Pick<Person, "handle" | "displayName">) => person.displayName || `@${person.handle}`;
|
|
254
|
-
const dates = new Intl.DateTimeFormat(
|
|
314
|
+
const dates = new Intl.DateTimeFormat(lang, {
|
|
255
315
|
day: "numeric",
|
|
256
316
|
month: "long",
|
|
257
317
|
year: "numeric",
|
|
@@ -280,7 +340,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
280
340
|
...groups.map(({ kind, count, people }) => {
|
|
281
341
|
const group = make("div", "atmo-reactions__group");
|
|
282
342
|
group.dataset.reaction = kind;
|
|
283
|
-
group.append(make("p", "atmo-reactions__count",
|
|
343
|
+
group.append(make("p", "atmo-reactions__count", t(kind === "like" ? "reactions.likes" : "reactions.reposts", { count })));
|
|
284
344
|
const faces = make("ul", "atmo-facepile");
|
|
285
345
|
for (const person of people) {
|
|
286
346
|
const link = Object.assign(make("a"), { href: person.url, title: nameOf(person), rel: "ugc nofollow noopener" });
|
|
@@ -313,7 +373,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
313
373
|
if (reply.byAuthor) who.append(make("span", "atmo-comment__badge", authorLabel));
|
|
314
374
|
const date = Object.assign(make("a", "atmo-comment__date"), { href: reply.url, rel: "ugc nofollow noopener" });
|
|
315
375
|
const time = Object.assign(make("time"), { dateTime: reply.createdAt, textContent: reply.createdAt ? dates.format(new Date(reply.createdAt)) : "" });
|
|
316
|
-
date.append(time, make("span", "",
|
|
376
|
+
date.append(time, make("span", "", ` · ${t("comment.viewOnBluesky")}`));
|
|
317
377
|
const names = make("div");
|
|
318
378
|
names.append(who, date);
|
|
319
379
|
const header = make("header", "atmo-comment__header");
|
|
@@ -359,6 +419,11 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
359
419
|
}
|
|
360
420
|
thread = data.thread;
|
|
361
421
|
post = thread.uri;
|
|
422
|
+
const elsewhere = this.querySelector<HTMLElement>("[data-elsewhere]");
|
|
423
|
+
if (elsewhere && thread.url) {
|
|
424
|
+
elsewhere.querySelector("a")!.href = thread.url;
|
|
425
|
+
elsewhere.hidden = false;
|
|
426
|
+
}
|
|
362
427
|
renderReactions();
|
|
363
428
|
repliesList.replaceChildren(...thread.replies.map(replyElement));
|
|
364
429
|
} catch {
|
|
@@ -404,19 +469,100 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
404
469
|
renderReactions();
|
|
405
470
|
};
|
|
406
471
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
loginForm.querySelector("
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
472
|
+
const openLogin = () => loginForm.querySelector("input")?.focus({ preventScroll: true });
|
|
473
|
+
|
|
474
|
+
// Matching accounts under the handle box as the reader types, with their
|
|
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
|
+
}
|
|
420
566
|
|
|
421
567
|
// An avatar picture, or the name's first letter if there isn't one.
|
|
422
568
|
const setAvatar = (slot: HTMLElement, name: string, src?: string) => {
|
|
@@ -429,13 +575,13 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
429
575
|
|
|
430
576
|
const handleError = (error: string, tell: (message: string) => void = say) => {
|
|
431
577
|
if (error === "signed-out" || error === "new-permissions") void show(false);
|
|
432
|
-
tell(
|
|
578
|
+
tell(message(error));
|
|
433
579
|
};
|
|
434
580
|
|
|
435
581
|
const postedMessage = (where: HTMLElement, data: Posted) =>
|
|
436
582
|
where.replaceChildren(
|
|
437
|
-
data.quote ? "
|
|
438
|
-
Object.assign(document.createElement("a"), { href: data.url, textContent: "
|
|
583
|
+
`${t(data.quote ? "posted.quote" : "posted.comment")} `,
|
|
584
|
+
Object.assign(document.createElement("a"), { href: data.url, textContent: t("posted.see") }),
|
|
439
585
|
);
|
|
440
586
|
|
|
441
587
|
// Everything a comment box does: counting characters, @mention
|
|
@@ -456,7 +602,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
456
602
|
|
|
457
603
|
const updateCount = () => {
|
|
458
604
|
const used = count(textarea.value);
|
|
459
|
-
counter.textContent = used > MAX - 50 ?
|
|
605
|
+
counter.textContent = used > MAX - 50 ? t("composer.charactersLeft", { count: MAX - used }) : "";
|
|
460
606
|
counter.classList.toggle("is-over", used > MAX);
|
|
461
607
|
};
|
|
462
608
|
|
|
@@ -563,9 +709,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
563
709
|
event.preventDefault();
|
|
564
710
|
const text = textarea.value.trim();
|
|
565
711
|
if (!text) return;
|
|
566
|
-
if (count(text) > MAX) return behaviour.tell(
|
|
712
|
+
if (count(text) > MAX) return behaviour.tell(message("too-long"));
|
|
567
713
|
submit.disabled = true;
|
|
568
|
-
behaviour.tell("
|
|
714
|
+
behaviour.tell(t("composer.posting"));
|
|
569
715
|
try {
|
|
570
716
|
const response = await send("comment", { text, root: post, ...behaviour.extra() });
|
|
571
717
|
const data = await response.json();
|
|
@@ -576,7 +722,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
576
722
|
showPosted(data);
|
|
577
723
|
this.dispatchEvent(new CustomEvent("atmosphere:commented", { detail: data, bubbles: true }));
|
|
578
724
|
} catch {
|
|
579
|
-
behaviour.tell(
|
|
725
|
+
behaviour.tell(message("error"));
|
|
580
726
|
} finally {
|
|
581
727
|
submit.disabled = false;
|
|
582
728
|
}
|
|
@@ -592,7 +738,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
592
738
|
let quotedLoaded = false;
|
|
593
739
|
const updatePreviewText = () => {
|
|
594
740
|
const words = main.textarea.value.trim();
|
|
595
|
-
previewText.textContent = words || "
|
|
741
|
+
previewText.textContent = words || t("composer.previewEmpty");
|
|
596
742
|
previewText.classList.toggle("is-placeholder", !words);
|
|
597
743
|
};
|
|
598
744
|
const loadQuoted = async () => {
|
|
@@ -634,9 +780,9 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
634
780
|
mainMode = next;
|
|
635
781
|
const quoting = next === "quote";
|
|
636
782
|
$<HTMLElement>("[data-context]").hidden = !quoting;
|
|
637
|
-
$<HTMLElement>("[data-context-text]").textContent = quoting ? "
|
|
638
|
-
$<HTMLElement>("[data-verb]").textContent = quoting ? "
|
|
639
|
-
$<HTMLElement>("[data-submit]").textContent = quoting ? "
|
|
783
|
+
$<HTMLElement>("[data-context-text]").textContent = quoting ? t("composer.quoting") : "";
|
|
784
|
+
$<HTMLElement>("[data-verb]").textContent = t(quoting ? "composer.quote" : "composer.comment");
|
|
785
|
+
$<HTMLElement>("[data-submit]").textContent = t(quoting ? "composer.postQuote" : "composer.postComment");
|
|
640
786
|
preview.hidden = !quoting;
|
|
641
787
|
if (quoting) void loadQuoted();
|
|
642
788
|
};
|
|
@@ -677,8 +823,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
677
823
|
suggestions.id = `${id}-suggestions`;
|
|
678
824
|
textarea.setAttribute("aria-controls", suggestions.id);
|
|
679
825
|
form.querySelector<HTMLElement>("[data-context]")!.hidden = false;
|
|
680
|
-
form.querySelector("[data-context-text]")!.textContent =
|
|
681
|
-
form.querySelector("[data-verb]")!.textContent = "
|
|
826
|
+
form.querySelector("[data-context-text]")!.textContent = t("composer.replyingTo", { name: author });
|
|
827
|
+
form.querySelector("[data-verb]")!.textContent = t("comment.reply");
|
|
682
828
|
form.querySelector("[data-submit]")!.textContent = "Post reply";
|
|
683
829
|
form.querySelector("[data-handle]")!.textContent = me.handle ? `@${me.handle}` : "";
|
|
684
830
|
setAvatar(form.querySelector<HTMLElement>("[data-me-avatar]")!, me.displayName || me.handle || "?", me.avatar);
|
|
@@ -717,8 +863,8 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
717
863
|
button.dataset.kind = kind;
|
|
718
864
|
const seen = known[comment.dataset.atmosphereUri!];
|
|
719
865
|
button.setAttribute("aria-pressed", String(!!(kind === "like" ? seen?.liked : seen?.reposted)));
|
|
720
|
-
const author = comment.dataset.atmosphereAuthor || "
|
|
721
|
-
button.setAttribute("aria-label",
|
|
866
|
+
const author = comment.dataset.atmosphereAuthor || t("comment.someone");
|
|
867
|
+
button.setAttribute("aria-label", t(kind === "like" ? "comment.likeLabel" : "comment.repostLabel", { name: author }));
|
|
722
868
|
const count = Number(comment.dataset[kind === "like" ? "atmosphereLikes" : "atmosphereReposts"] || 0);
|
|
723
869
|
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${kind === "like" ? HEART : ARROWS}</svg><span></span>`;
|
|
724
870
|
const setCount = (value: number) => {
|
|
@@ -727,7 +873,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
727
873
|
};
|
|
728
874
|
setCount(count);
|
|
729
875
|
button.addEventListener("click", async () => {
|
|
730
|
-
if (!isSignedIn) return signInFirst(
|
|
876
|
+
if (!isSignedIn) return signInFirst(t(kind === "like" ? "signInFirst.like" : "signInFirst.repost"));
|
|
731
877
|
const on = button.getAttribute("aria-pressed") !== "true";
|
|
732
878
|
const before = Number(button.dataset.count || 0);
|
|
733
879
|
button.setAttribute("aria-pressed", String(on));
|
|
@@ -751,23 +897,103 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
751
897
|
if (comment.querySelector(":scope .atmo-comment-actions")) return;
|
|
752
898
|
added += 1;
|
|
753
899
|
const actions = Object.assign(document.createElement("div"), { className: "atmo-comment-actions" });
|
|
754
|
-
const author = comment.dataset.atmosphereAuthor || "
|
|
900
|
+
const author = comment.dataset.atmosphereAuthor || t("comment.someone");
|
|
755
901
|
const reply = Object.assign(document.createElement("button"), {
|
|
756
902
|
type: "button",
|
|
757
903
|
className: "atmo-comment-action",
|
|
758
|
-
textContent: "
|
|
904
|
+
textContent: t("comment.reply"),
|
|
759
905
|
});
|
|
760
|
-
reply.setAttribute("aria-label",
|
|
906
|
+
reply.setAttribute("aria-label", t("comment.replyTo", { name: author }));
|
|
761
907
|
reply.addEventListener("click", () => {
|
|
762
|
-
if (!isSignedIn) return signInFirst("
|
|
908
|
+
if (!isSignedIn) return signInFirst(t("signInFirst.reply"));
|
|
763
909
|
openReply(comment, author);
|
|
764
910
|
});
|
|
765
911
|
actions.append(reply, commentButton("like", comment), commentButton("repost", comment));
|
|
766
912
|
(comment.querySelector("article") ?? comment).append(actions);
|
|
767
913
|
});
|
|
914
|
+
addDeleteButtons();
|
|
768
915
|
return added;
|
|
769
916
|
};
|
|
770
917
|
|
|
918
|
+
// Delete, under the signed-in reader's own comments only.
|
|
919
|
+
const isMine = (comment: HTMLElement) => !!me.did && !!comment.dataset.atmosphereUri?.startsWith(`at://${me.did}/`);
|
|
920
|
+
|
|
921
|
+
const deleteComment = async (comment: HTMLElement, panel: HTMLElement) => {
|
|
922
|
+
const uri = comment.dataset.atmosphereUri!;
|
|
923
|
+
panel.replaceChildren(make("span", "atmo-comment-delete__text", t("comment.deleting")));
|
|
924
|
+
try {
|
|
925
|
+
const response = await send("delete", { post: uri });
|
|
926
|
+
const data = (await response.json()) as { error?: string };
|
|
927
|
+
if (response.ok) {
|
|
928
|
+
comment.remove();
|
|
929
|
+
say(t("comment.deleted"));
|
|
930
|
+
this.dispatchEvent(new CustomEvent("atmosphere:deleted", { detail: { uri }, bubbles: true }));
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (data.error === "needs-permission") return askDeletePermission(comment, panel);
|
|
934
|
+
panel.remove();
|
|
935
|
+
handleError(data.error ?? "error");
|
|
936
|
+
} catch {
|
|
937
|
+
panel.remove();
|
|
938
|
+
say(message("error"));
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// Deleting posts isn't part of signing in, so the first delete asks the
|
|
943
|
+
// reader's account host for it, then carries on when they come back.
|
|
944
|
+
const askDeletePermission = (comment: HTMLElement, panel: HTMLElement) => {
|
|
945
|
+
const form = Object.assign(document.createElement("form"), { method: "post", action: `${base}/login` });
|
|
946
|
+
const field = (name: string, value: string) =>
|
|
947
|
+
form.append(Object.assign(document.createElement("input"), { type: "hidden", name, value }));
|
|
948
|
+
field("handle", me.did!);
|
|
949
|
+
field("permission", "delete");
|
|
950
|
+
field("returnTo", `${location.pathname}#atmo-delete=${encodeURIComponent(comment.dataset.atmosphereUri!)}`);
|
|
951
|
+
const go = Object.assign(make("button", "atmo-comment-action atmo-comment-action--danger", t("comment.continue")), { type: "submit" });
|
|
952
|
+
const cancel = Object.assign(make("button", "atmo-comment-action", t("comment.cancel")), { type: "button" });
|
|
953
|
+
cancel.addEventListener("click", () => panel.remove());
|
|
954
|
+
form.append(go, cancel);
|
|
955
|
+
panel.replaceChildren(
|
|
956
|
+
make(
|
|
957
|
+
"span",
|
|
958
|
+
"atmo-comment-delete__text",
|
|
959
|
+
t("comment.deletePermission"),
|
|
960
|
+
),
|
|
961
|
+
form,
|
|
962
|
+
);
|
|
963
|
+
go.focus();
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const deleteButton = (comment: HTMLElement) => {
|
|
967
|
+
const button = Object.assign(make("button", "atmo-comment-action atmo-comment-action--delete", t("comment.delete")), { type: "button" });
|
|
968
|
+
button.setAttribute("aria-label", t("comment.deleteLabel"));
|
|
969
|
+
button.addEventListener("click", () => {
|
|
970
|
+
const actions = button.parentElement!;
|
|
971
|
+
if (actions.nextElementSibling?.matches(".atmo-comment-delete")) return;
|
|
972
|
+
const panel = make("div", "atmo-comment-delete");
|
|
973
|
+
const yes = Object.assign(make("button", "atmo-comment-action atmo-comment-action--danger", t("comment.delete")), { type: "button" });
|
|
974
|
+
const no = Object.assign(make("button", "atmo-comment-action", t("comment.cancel")), { type: "button" });
|
|
975
|
+
yes.addEventListener("click", () => deleteComment(comment, panel));
|
|
976
|
+
no.addEventListener("click", () => {
|
|
977
|
+
panel.remove();
|
|
978
|
+
button.focus();
|
|
979
|
+
});
|
|
980
|
+
panel.append(make("span", "atmo-comment-delete__text", t("comment.deleteConfirm")), yes, no);
|
|
981
|
+
panel.setAttribute("role", "group");
|
|
982
|
+
panel.setAttribute("aria-label", t("comment.deleteLabel"));
|
|
983
|
+
actions.after(panel);
|
|
984
|
+
no.focus();
|
|
985
|
+
});
|
|
986
|
+
return button;
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const addDeleteButtons = () => {
|
|
990
|
+
document.querySelectorAll<HTMLElement>("[data-atmosphere-uri]").forEach((comment) => {
|
|
991
|
+
const actions = comment.querySelector<HTMLElement>(":scope .atmo-comment-actions");
|
|
992
|
+
if (!actions || actions.querySelector(".atmo-comment-action--delete")) return;
|
|
993
|
+
if (isSignedIn && isMine(comment)) actions.append(deleteButton(comment));
|
|
994
|
+
});
|
|
995
|
+
};
|
|
996
|
+
|
|
771
997
|
// Which of the post and its comments the reader has already liked or reposted.
|
|
772
998
|
const loadState = async () => {
|
|
773
999
|
if (!post) return;
|
|
@@ -809,16 +1035,33 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
809
1035
|
isSignedIn = signed;
|
|
810
1036
|
signedOut.hidden = signed;
|
|
811
1037
|
signedIn.hidden = !signed;
|
|
812
|
-
if (!signed)
|
|
1038
|
+
if (!signed) {
|
|
1039
|
+
document.querySelectorAll(".atmo-comment-action--delete, .atmo-comment-delete").forEach((part) => part.remove());
|
|
1040
|
+
return closeReplies();
|
|
1041
|
+
}
|
|
813
1042
|
addCommentActions();
|
|
814
1043
|
await loadState();
|
|
1044
|
+
// Back from approving the delete permission: finish the delete they asked for.
|
|
1045
|
+
const pending = location.hash.match(/^#atmo-delete=(.+)$/);
|
|
1046
|
+
if (pending) {
|
|
1047
|
+
history.replaceState(history.state, "", `${location.pathname}${location.search}#atmosphere-comments`);
|
|
1048
|
+
const uri = decodeURIComponent(pending[1]);
|
|
1049
|
+
const comment = document.querySelector<HTMLElement>(`[data-atmosphere-uri="${CSS.escape(uri)}"]`);
|
|
1050
|
+
const actions = comment?.querySelector<HTMLElement>(":scope .atmo-comment-actions");
|
|
1051
|
+
if (comment && actions && isMine(comment)) {
|
|
1052
|
+
const panel = make("div", "atmo-comment-delete");
|
|
1053
|
+
actions.after(panel);
|
|
1054
|
+
comment.scrollIntoView({ block: "center" });
|
|
1055
|
+
await deleteComment(comment, panel);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
815
1058
|
};
|
|
816
1059
|
|
|
817
1060
|
// A message from the sign-in round trip (?atmosphere=…), then tidy the address.
|
|
818
1061
|
const url = new URL(location.href);
|
|
819
1062
|
const result = url.searchParams.get("atmosphere");
|
|
820
1063
|
if (result) {
|
|
821
|
-
say(
|
|
1064
|
+
say(MESSAGE_KEYS[result] ? message(result) : "");
|
|
822
1065
|
url.searchParams.delete("atmosphere");
|
|
823
1066
|
history.replaceState(history.state, "", url);
|
|
824
1067
|
}
|
|
@@ -857,7 +1100,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
857
1100
|
setPressed(kind, !on);
|
|
858
1101
|
return handleError((await response.json()).error);
|
|
859
1102
|
}
|
|
860
|
-
say(on ? (kind === "like" ? "
|
|
1103
|
+
say(t(on ? (kind === "like" ? "status.liked" : "status.reposted") : kind === "like" ? "status.likeRemoved" : "status.repostRemoved"));
|
|
861
1104
|
showReaction(kind, on);
|
|
862
1105
|
this.dispatchEvent(
|
|
863
1106
|
new CustomEvent("atmosphere:reacted", {
|
|
@@ -867,7 +1110,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
867
1110
|
);
|
|
868
1111
|
} catch {
|
|
869
1112
|
setPressed(kind, !on);
|
|
870
|
-
say(
|
|
1113
|
+
say(message("error"));
|
|
871
1114
|
}
|
|
872
1115
|
});
|
|
873
1116
|
}
|
|
@@ -876,7 +1119,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
876
1119
|
await fetch(`${base}/logout`, { method: "POST", credentials: "same-origin" }).catch(() => undefined);
|
|
877
1120
|
void show(false);
|
|
878
1121
|
setMainMode("comment");
|
|
879
|
-
say("
|
|
1122
|
+
say(t("status.signedOut"));
|
|
880
1123
|
});
|
|
881
1124
|
}
|
|
882
1125
|
}
|
|
@@ -900,50 +1143,49 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
900
1143
|
display: none !important;
|
|
901
1144
|
}
|
|
902
1145
|
|
|
903
|
-
/* The
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
border-radius: 12px;
|
|
912
|
-
background: none;
|
|
913
|
-
color: var(--atmo-text, currentColor);
|
|
914
|
-
font-family: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
915
|
-
text-align: left;
|
|
916
|
-
cursor: pointer;
|
|
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;
|
|
917
1154
|
}
|
|
918
1155
|
|
|
919
|
-
.
|
|
920
|
-
.
|
|
921
|
-
|
|
922
|
-
color: var(--atmo-accent, #1185fe);
|
|
1156
|
+
/* Matching accounts sit under the handle box, as wide as it. */
|
|
1157
|
+
.atmo__handle .atmo__suggestions {
|
|
1158
|
+
width: 100%;
|
|
923
1159
|
}
|
|
924
1160
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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);
|
|
931
1169
|
mask: var(--atmo-logo) center / contain no-repeat;
|
|
1170
|
+
pointer-events: none;
|
|
932
1171
|
}
|
|
933
1172
|
|
|
934
|
-
.
|
|
935
|
-
|
|
936
|
-
line-height: 1.15;
|
|
1173
|
+
.atmo .atmo__handle input[type="text"] {
|
|
1174
|
+
padding-left: 40px;
|
|
937
1175
|
}
|
|
938
1176
|
|
|
939
|
-
.
|
|
940
|
-
|
|
1177
|
+
.atmo__button--sign-in {
|
|
1178
|
+
display: inline-flex;
|
|
1179
|
+
align-items: center;
|
|
1180
|
+
gap: 8px;
|
|
941
1181
|
}
|
|
942
1182
|
|
|
943
|
-
.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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;
|
|
947
1189
|
}
|
|
948
1190
|
|
|
949
1191
|
/* The comment box when it sits under a comment being replied to. */
|
|
@@ -955,6 +1197,16 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
955
1197
|
margin-top: 10px;
|
|
956
1198
|
}
|
|
957
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
|
+
|
|
958
1210
|
.atmo__login,
|
|
959
1211
|
.atmo__composer {
|
|
960
1212
|
display: grid;
|
|
@@ -1111,7 +1363,7 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1111
1363
|
opacity: 0.8;
|
|
1112
1364
|
}
|
|
1113
1365
|
|
|
1114
|
-
.atmo__credit a {
|
|
1366
|
+
.atmo__credit :global(a) {
|
|
1115
1367
|
color: inherit;
|
|
1116
1368
|
}
|
|
1117
1369
|
|
|
@@ -1459,6 +1711,24 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1459
1711
|
margin-top: 8px;
|
|
1460
1712
|
}
|
|
1461
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
|
+
|
|
1462
1732
|
:global(.atmo-comment-action) {
|
|
1463
1733
|
display: inline-flex;
|
|
1464
1734
|
align-items: center;
|
|
@@ -1473,6 +1743,15 @@ const returnTo = `${Astro.url.pathname}#atmosphere-comments`;
|
|
|
1473
1743
|
cursor: pointer;
|
|
1474
1744
|
}
|
|
1475
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
|
+
|
|
1476
1755
|
:global(.atmo-comment-action:hover),
|
|
1477
1756
|
:global(.atmo-comment-action:focus-visible),
|
|
1478
1757
|
:global(.atmo-comment-action[aria-pressed="true"]) {
|