@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala/theme",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Managed Gala static-site theme payload",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "themePackage": {
4
4
  "name": "@rathnasgala/theme",
5
- "version": "0.0.10",
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": "a37a0dfe534412bcade43719b2e2d6338463110105f84f3887e21ecc27e44b6c",
28
+ "src/_includes/layouts/base.njk": "1bd300b68db1786b593ae5ef4325d68f21a5c79a78ac1cb3aba09fdba57bec9d",
29
29
  "src/_includes/layouts/post.njk": "1accd2832064ad19ee7bf18daf5fa1e60569f32c9b7a547934a6361f747d7eb7",
30
- "src/_includes/components/ui.njk": "3a0eeb0f7b8f0ea46ed39d4c53b810a33b997766cbae540ef063c8ecdf641809",
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": "7b9db09494518813189c17e92e5b9621b8b17a455c895d8670452fdfa0ead557",
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": "66f4793e857af61cbee63d847418aac03ce948361025d78853ec4d05e0117f43",
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
- <section class="gala-engagement-actions" aria-label="Reader actions" data-engagement-actions hidden>
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>&copy; {{ site.site.name }}</p>
59
- {% if attributionTier != 'PAID' %}<p><a href="https://gala67.com/?utm_source=gala-site&amp;utm_medium=referral" rel="external">Built with Gala</a></p>{% endif %}
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&amp;utm_medium=referral&amp;utm_campaign=built-with-gala&amp;utm_content={{ galaLinkPlacement | urlencode }}&amp;utm_term={{ galaLinkLanguage | urlencode }}&amp;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
- const actions = region.querySelector('[data-engagement-actions]');
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,