@rathnasgala/theme 0.0.10 → 0.0.11
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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"themePackage": {
|
|
4
4
|
"name": "@rathnasgala/theme",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.11",
|
|
6
6
|
"availableDesignThemes": [
|
|
7
7
|
"editorial"
|
|
8
8
|
],
|
|
@@ -25,23 +25,23 @@
|
|
|
25
25
|
"lib/provider-fixtures/embeds.v1.json": "5f26909fb433135321ddf7fdd14e45aeb702805c88f5676941166c5735143ee2",
|
|
26
26
|
"lib/provider-fixtures/share-intents.v1.json": "66a8eedfcff711253ee45ceb94b53f7d5a6be3ddbbb3d1f0d4e31a114326cc0a",
|
|
27
27
|
"src/404.njk": "be0d44370ac5402156aaf3d32ea581057bc61ec94c1a5f92bb809733af7a8a20",
|
|
28
|
-
"src/_includes/layouts/base.njk": "
|
|
28
|
+
"src/_includes/layouts/base.njk": "b62dea9dcd5eeefc020e1823ce8afc7d5b0105df56d57fdcacbf414f4edb4dd2",
|
|
29
29
|
"src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
|
|
30
|
-
"src/_includes/components/ui.njk": "
|
|
30
|
+
"src/_includes/components/ui.njk": "de062ca818759edb53dc5e94ab839ac9453fda1fe458c61c04d9d89ef880c6af",
|
|
31
31
|
"src/_data/buildManifest.js": "628163305984e0d3537e27b0d4050f5ba5ead12a834878266d2ce8004ffe1b1e",
|
|
32
32
|
"src/_data/engagementSnapshot.js": "9217dacebfaf008f7a0ad67ff4e8d1fa8fba609a9dc7e3430c78c5b32024da0e",
|
|
33
33
|
"src/_data/feedLinks.js": "06c93352ebd888acb431a1c9a8be6ecff757895468e9f2d336761038751c2d5e",
|
|
34
34
|
"src/_data/languages.js": "1358748f7fa2f74406d3295a030827d6229a53e94da72cf712e719d7a356aeac",
|
|
35
35
|
"src/_data/site.js": "d9c001f4cc12a34027dd92303530b11b5402d47613459bb997e22c4bcbea326d",
|
|
36
36
|
"src/assets/theme-mode.js": "3797c84dbaa090b5b21427a31928b5dd97f13d04d99833f5e1e74c6b50e01dcc",
|
|
37
|
-
"src/assets/interactions.js": "
|
|
37
|
+
"src/assets/interactions.js": "c8ea01683941da3f81536a9c5d33d2e8d09a1f31f6090d33b53b70fa7aad621d",
|
|
38
38
|
"src/assets/embed-codepen.svg": "e9def697f38c466fb2b6652535c4c814e8cbbbefc6e052a69f335aeb27e8356b",
|
|
39
39
|
"src/assets/embed-gist.svg": "fcd9bc2bf29a5c118492b853c0a01390b7539c38ddb985da3c2a6e2f246a39f7",
|
|
40
40
|
"src/assets/embed-x.svg": "0ae74edc6790ec7e933067fe5392bce756e93fa7ad71caacf1c1ca99fa4ec9b6",
|
|
41
41
|
"src/assets/embed-youtube.svg": "10d192f2b14b67664fea88c8cf246321d20190def51d59fad5762e552219b067",
|
|
42
42
|
"src/assets/preferences.js": "789127bee013ed6a1bbf8cefd7b9109291c16220e7a6e09314e3c93c54731d1d",
|
|
43
43
|
"src/assets/search.js": "df425f0239277792ad39f2138bc89cb66cc4ecda5ed59042d46abc2f05372088",
|
|
44
|
-
"src/assets/theme.css": "
|
|
44
|
+
"src/assets/theme.css": "2c835fb79531d7b4a71bc2d41a6deb6e6b3689bf8603d903a3ba93add66c0d60",
|
|
45
45
|
"src/contact.njk": "f4d74d038781e15af0acef571901c91e7753a2e21b6bef617429585698839a34",
|
|
46
46
|
"src/index.njk": "5c313f0fcdad3ddbfb79376bc9671b3f37539ef70c14bdbbd26d6e2e6de250e8",
|
|
47
47
|
"src/feed.11ty.js": "009bd2384e08e02a9405683bab1653bab3aeaec16accb0fd01ba69b24c777da9",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
<section class="gala-engagement" aria-label="Engagement" data-article-id="{{ articleId | escape }}" data-engagement-url="{{ apiBaseUrl | escape }}/v1/articles/{{ articleId | escape }}/engagement">
|
|
18
18
|
{% if engagement.available %}<dl data-engagement-snapshot><div><dt>Reactions</dt><dd>{{ engagement.reactions }}</dd></div><div><dt>Comments</dt><dd>{{ engagement.comments }}</dd></div><div><dt>Views</dt><dd>{{ engagement.views }}</dd></div></dl>{% else %}<p class="gala-engagement__placeholder" role="status">Loading engagement data.</p>{% endif %}
|
|
19
19
|
<div data-engagement-live></div>
|
|
20
|
-
|
|
20
|
+
{# Readers see what they can do before they are asked who they are. Signing in is requested
|
|
21
|
+
at the moment of intent, and the intent is replayed once they are back. #}
|
|
22
|
+
<section class="gala-engagement-actions" aria-label="Reader actions" data-engagement-actions>
|
|
21
23
|
<div class="gala-reaction-actions" role="group" aria-label="React to this article">
|
|
22
24
|
{% for reaction in ['like', 'love', 'insightful', 'curious', 'celebrate', 'support'] %}<button type="button" data-reaction="{{ reaction }}" aria-pressed="false">{{ reaction | capitalize }}</button>{% endfor %}
|
|
23
25
|
</div>
|
|
@@ -29,7 +31,6 @@
|
|
|
29
31
|
<button type="button" data-cancel-reply hidden>Cancel reply</button>
|
|
30
32
|
</form>
|
|
31
33
|
</section>
|
|
32
|
-
<p data-engagement-sign-in>Sign in from the account menu to comment, react, or follow.</p>
|
|
33
34
|
<output data-engagement-status aria-live="polite"></output>
|
|
34
35
|
</section>
|
|
35
36
|
{% endmacro %}
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
<dialog id="gala-account-dialog" class="gala-dialog gala-account-dialog" aria-labelledby="gala-account-title">
|
|
92
92
|
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close account">Close</button></form>
|
|
93
93
|
<h2 id="gala-account-title">Gala account</h2>
|
|
94
|
+
<p class="gala-account-dialog__invite" data-account-invite hidden>Takes a couple of clicks to authenticate yourself. No passwords, no spam.</p>
|
|
94
95
|
<iframe data-gala-session-frame title="Gala account and sign in" allow="identity-credentials-get" src="{{ site.platform.apiBaseUrl or 'https://api.gala67.com' }}/v1/widget/session?siteId={{ site.site.id | escape }}"></iframe>
|
|
95
96
|
</dialog>
|
|
96
97
|
{% endif %}
|
|
@@ -39,6 +39,7 @@ document.addEventListener('click', async (event) => {
|
|
|
39
39
|
if (reaction) {
|
|
40
40
|
const region = reaction.closest('[data-engagement-url]');
|
|
41
41
|
if (!region) return;
|
|
42
|
+
if (!sessionUser) return requestSession({ kind: 'reaction', element: reaction });
|
|
42
43
|
const active = reaction.getAttribute('aria-pressed') !== 'true';
|
|
43
44
|
const saved = await mutateEngagement(region, active ? 'reaction.add' : 'reaction.remove', {
|
|
44
45
|
articleId: region.dataset.articleId,
|
|
@@ -53,6 +54,7 @@ document.addEventListener('click', async (event) => {
|
|
|
53
54
|
if (follow) {
|
|
54
55
|
const region = follow.closest('[data-engagement-url]');
|
|
55
56
|
if (!region) return;
|
|
57
|
+
if (!sessionUser) return requestSession({ kind: 'follow', element: follow });
|
|
56
58
|
const active = follow.getAttribute('aria-pressed') !== 'true';
|
|
57
59
|
const saved = await mutateEngagement(region, active ? 'follow.add' : 'follow.remove', {
|
|
58
60
|
targetType: 'articles', targetId: region.dataset.articleId
|
|
@@ -351,6 +353,14 @@ function recordView(region) {
|
|
|
351
353
|
}).catch(() => {});
|
|
352
354
|
}
|
|
353
355
|
|
|
356
|
+
// Asked at the moment of intent: the reader reaches for the comment box, and only then are they
|
|
357
|
+
// asked who they are — never as a wall in front of something they have not decided to do.
|
|
358
|
+
document.addEventListener('focusin', (event) => {
|
|
359
|
+
if (sessionUser || pendingIntent) return;
|
|
360
|
+
const field = event.target.closest('[data-comment-create] textarea, [data-comment-editor] textarea');
|
|
361
|
+
if (field) requestSession({ kind: 'comment', element: field });
|
|
362
|
+
});
|
|
363
|
+
|
|
354
364
|
document.addEventListener('submit', async (event) => {
|
|
355
365
|
const create = event.target.closest('[data-comment-create]');
|
|
356
366
|
const editor = event.target.closest('[data-comment-editor]');
|
|
@@ -360,6 +370,9 @@ document.addEventListener('submit', async (event) => {
|
|
|
360
370
|
const region = form.closest('[data-engagement-url]');
|
|
361
371
|
const body = form.elements.body?.value;
|
|
362
372
|
if (!region || typeof body !== 'string' || body.trim() === '') return;
|
|
373
|
+
if (!sessionUser) {
|
|
374
|
+
return requestSession({ kind: 'comment', element: form.querySelector('textarea') });
|
|
375
|
+
}
|
|
363
376
|
if (create) {
|
|
364
377
|
const saved = await mutateEngagement(region, 'comment.create', {
|
|
365
378
|
articleId: region.dataset.articleId,
|
|
@@ -381,8 +394,55 @@ document.addEventListener('submit', async (event) => {
|
|
|
381
394
|
});
|
|
382
395
|
|
|
383
396
|
let sessionUser = null;
|
|
397
|
+
// The reader the current engagement render was built for: null until the frame reports, and
|
|
398
|
+
// null again for a signed-out reader, so an anonymous load never re-requests.
|
|
399
|
+
let renderedSessionUser = null;
|
|
384
400
|
const pendingEngagementWrites = new Map();
|
|
385
401
|
|
|
402
|
+
/**
|
|
403
|
+
* What the reader was doing when we had to ask who they are. Signing in is an interruption, so
|
|
404
|
+
* the interruption has to end exactly where it started: the reaction they pressed gets applied,
|
|
405
|
+
* and the comment they were typing keeps its text and its caret.
|
|
406
|
+
*/
|
|
407
|
+
let pendingIntent = null;
|
|
408
|
+
|
|
409
|
+
function requestSession(intent) {
|
|
410
|
+
const dialog = document.getElementById('gala-account-dialog');
|
|
411
|
+
const field = intent.element;
|
|
412
|
+
pendingIntent = {
|
|
413
|
+
kind: intent.kind,
|
|
414
|
+
element: field,
|
|
415
|
+
// Held here rather than left in the DOM, because signing in re-renders the region.
|
|
416
|
+
draft: field && 'value' in field ? field.value : null,
|
|
417
|
+
caret: field && 'selectionStart' in field ? field.selectionStart : null
|
|
418
|
+
};
|
|
419
|
+
if (!(dialog instanceof HTMLDialogElement)) return;
|
|
420
|
+
const invite = dialog.querySelector('[data-account-invite]');
|
|
421
|
+
if (invite) invite.hidden = false;
|
|
422
|
+
if (field && typeof field.blur === 'function') field.blur();
|
|
423
|
+
if (!dialog.open) dialog.showModal();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Puts the reader back exactly where the sign-in interrupted them. */
|
|
427
|
+
function resumeIntent() {
|
|
428
|
+
const intent = pendingIntent;
|
|
429
|
+
pendingIntent = null;
|
|
430
|
+
if (!intent) return;
|
|
431
|
+
const element = intent.element;
|
|
432
|
+
if (!element || !element.isConnected) return;
|
|
433
|
+
if (intent.kind === 'comment') {
|
|
434
|
+
if ('value' in element && intent.draft != null && element.value === '') element.value = intent.draft;
|
|
435
|
+
element.focus();
|
|
436
|
+
if (intent.caret != null && 'setSelectionRange' in element) {
|
|
437
|
+
element.setSelectionRange(intent.caret, intent.caret);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// A reaction or follow was a completed decision, so carry it out rather than make them press
|
|
442
|
+
// the same thing twice.
|
|
443
|
+
element.click();
|
|
444
|
+
}
|
|
445
|
+
|
|
386
446
|
document.querySelectorAll('[data-engagement-url]').forEach((region) => {
|
|
387
447
|
refreshEngagement(region);
|
|
388
448
|
recordView(region);
|
|
@@ -475,12 +535,21 @@ if (sessionFrame) {
|
|
|
475
535
|
? `Account: ${displayName}` : 'Sign in or view account');
|
|
476
536
|
control.title = displayName ? `Account: ${displayName}` : 'Account';
|
|
477
537
|
}
|
|
538
|
+
// The session frame reports on every page load, signed in or not. Re-reading engagement
|
|
539
|
+
// then duplicated the request made on load and returned an identical payload for anyone
|
|
540
|
+
// who was not signed in. Only a change of reader can change what the API answers.
|
|
541
|
+
const changed = renderedSessionUser !== (sessionUser?.id ?? null);
|
|
542
|
+
renderedSessionUser = sessionUser?.id ?? null;
|
|
478
543
|
document.querySelectorAll('[data-engagement-url]').forEach((region) => {
|
|
479
|
-
|
|
480
|
-
const signIn = region.querySelector('[data-engagement-sign-in]');
|
|
481
|
-
if (actions) actions.hidden = !sessionUser;
|
|
482
|
-
if (signIn) signIn.hidden = Boolean(sessionUser);
|
|
483
|
-
refreshEngagement(region);
|
|
544
|
+
if (changed) refreshEngagement(region);
|
|
484
545
|
});
|
|
546
|
+
// Signed in on the back of an interrupted action: close the interruption and finish it.
|
|
547
|
+
if (sessionUser && pendingIntent) {
|
|
548
|
+
const dialog = document.getElementById('gala-account-dialog');
|
|
549
|
+
const invite = dialog?.querySelector('[data-account-invite]');
|
|
550
|
+
if (invite) invite.hidden = true;
|
|
551
|
+
if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close();
|
|
552
|
+
resumeIntent();
|
|
553
|
+
}
|
|
485
554
|
});
|
|
486
555
|
}
|
|
@@ -180,6 +180,10 @@ button:focus-visible {
|
|
|
180
180
|
.gala-comments li { padding: 1rem; background: var(--gala-color-surface); border-radius: var(--gala-radius-control); }
|
|
181
181
|
.gala-engagement-actions { display: grid; gap: 1rem; margin-block-start: 1.5rem; }
|
|
182
182
|
.gala-engagement-actions[hidden] { display: none; }
|
|
183
|
+
/* Shown only when signing in interrupted something the reader had already decided to do, so it
|
|
184
|
+
reassures rather than sells. Hidden for anyone who opened the account dialog deliberately. */
|
|
185
|
+
.gala-account-dialog__invite { margin-block: 0 1rem; max-inline-size: 34rem; }
|
|
186
|
+
.gala-account-dialog__invite[hidden] { display: none; }
|
|
183
187
|
.gala-reaction-actions,
|
|
184
188
|
.gala-comment-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
185
189
|
.gala-engagement-actions form,
|