@rathnasgala/theme 0.0.9 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala/theme",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
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.9",
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": "a37a0dfe534412bcade43719b2e2d6338463110105f84f3887e21ecc27e44b6c",
28
+ "src/_includes/layouts/base.njk": "b62dea9dcd5eeefc020e1823ce8afc7d5b0105df56d57fdcacbf414f4edb4dd2",
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": "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": "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 %}
@@ -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
- 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);
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,