@rathnasgala/theme 0.0.10 → 0.0.12
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.12",
|
|
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": "1bd300b68db1786b593ae5ef4325d68f21a5c79a78ac1cb3aba09fdba57bec9d",
|
|
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": "3cd88a919bde8b894fe8509be8db1b07ddd9459df8b31dac83bf9b0215cd73df",
|
|
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 %}
|
|
@@ -56,7 +56,11 @@
|
|
|
56
56
|
<main id="main-content">{{ content | safe }}</main>
|
|
57
57
|
<footer class="gala-page-footer">
|
|
58
58
|
<p>© {{ site.site.name }}</p>
|
|
59
|
-
{
|
|
59
|
+
{# Which footer the click came from, which language it was read in, and whose site it was.
|
|
60
|
+
site.id is the identifier available to a built page and resolves to its owner. #}
|
|
61
|
+
{% set galaLinkPlacement = 'article-footer' if post else 'site-footer' %}
|
|
62
|
+
{% set galaLinkLanguage = post.language or site.site.defaultLanguage %}
|
|
63
|
+
{% if attributionTier != 'PAID' %}<p><a href="https://gala67.com/?utm_source=gala-site&utm_medium=referral&utm_campaign=built-with-gala&utm_content={{ galaLinkPlacement | urlencode }}&utm_term={{ galaLinkLanguage | urlencode }}&utm_id={{ site.site.id | urlencode }}" rel="external">Built with Gala</a></p>{% endif %}
|
|
60
64
|
</footer>
|
|
61
65
|
<dialog id="gala-settings-dialog" class="gala-dialog" aria-labelledby="gala-settings-title">
|
|
62
66
|
<form method="dialog" class="gala-dialog__close"><button type="submit" aria-label="Close settings">Close</button></form>
|
|
@@ -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,63 @@ 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 field = intent.element;
|
|
411
|
+
pendingIntent = {
|
|
412
|
+
kind: intent.kind,
|
|
413
|
+
element: field,
|
|
414
|
+
// Held here rather than left in the DOM, because signing in re-renders the region.
|
|
415
|
+
draft: field && 'value' in field ? field.value : null,
|
|
416
|
+
caret: field && 'selectionStart' in field ? field.selectionStart : null
|
|
417
|
+
};
|
|
418
|
+
// One window, not three. Asking used to open a modal, containing a frame, containing a
|
|
419
|
+
// button, that opened a popup. The popup is the only part that has to exist, because it is
|
|
420
|
+
// the only one running at the top level on the origin the identity providers accept.
|
|
421
|
+
const frame = document.querySelector('[data-gala-session-frame]');
|
|
422
|
+
const status = document.querySelector('[data-engagement-status]');
|
|
423
|
+
if (!frame) return;
|
|
424
|
+
const source = new URL(frame.getAttribute('src'), window.location.href);
|
|
425
|
+
const signIn = new URL('/v1/widget/session/sign-in', source.origin);
|
|
426
|
+
signIn.searchParams.set('siteId', source.searchParams.get('siteId') ?? '');
|
|
427
|
+
if (field && typeof field.blur === 'function') field.blur();
|
|
428
|
+
if (!window.open(signIn, 'gala-sign-in', 'popup,width=520,height=680')) {
|
|
429
|
+
pendingIntent = null;
|
|
430
|
+
if (status) status.textContent = 'Allow pop-ups for this site to sign in, then try again.';
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Puts the reader back exactly where the sign-in interrupted them. */
|
|
435
|
+
function resumeIntent() {
|
|
436
|
+
const intent = pendingIntent;
|
|
437
|
+
pendingIntent = null;
|
|
438
|
+
if (!intent) return;
|
|
439
|
+
const element = intent.element;
|
|
440
|
+
if (!element || !element.isConnected) return;
|
|
441
|
+
if (intent.kind === 'comment') {
|
|
442
|
+
if ('value' in element && intent.draft != null && element.value === '') element.value = intent.draft;
|
|
443
|
+
element.focus();
|
|
444
|
+
if (intent.caret != null && 'setSelectionRange' in element) {
|
|
445
|
+
element.setSelectionRange(intent.caret, intent.caret);
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// A reaction or follow was a completed decision, so carry it out rather than make them press
|
|
450
|
+
// the same thing twice.
|
|
451
|
+
element.click();
|
|
452
|
+
}
|
|
453
|
+
|
|
386
454
|
document.querySelectorAll('[data-engagement-url]').forEach((region) => {
|
|
387
455
|
refreshEngagement(region);
|
|
388
456
|
recordView(region);
|
|
@@ -475,12 +543,15 @@ if (sessionFrame) {
|
|
|
475
543
|
? `Account: ${displayName}` : 'Sign in or view account');
|
|
476
544
|
control.title = displayName ? `Account: ${displayName}` : 'Account';
|
|
477
545
|
}
|
|
546
|
+
// The session frame reports on every page load, signed in or not. Re-reading engagement
|
|
547
|
+
// then duplicated the request made on load and returned an identical payload for anyone
|
|
548
|
+
// who was not signed in. Only a change of reader can change what the API answers.
|
|
549
|
+
const changed = renderedSessionUser !== (sessionUser?.id ?? null);
|
|
550
|
+
renderedSessionUser = sessionUser?.id ?? null;
|
|
478
551
|
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);
|
|
552
|
+
if (changed) refreshEngagement(region);
|
|
484
553
|
});
|
|
554
|
+
// Signed in on the back of an interrupted action: finish what they were doing.
|
|
555
|
+
if (sessionUser && pendingIntent) resumeIntent();
|
|
485
556
|
});
|
|
486
557
|
}
|
|
@@ -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,
|