@rathnasgala/theme 0.0.16 → 0.0.18

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.
@@ -70,61 +70,6 @@ document.addEventListener('click', async (event) => {
70
70
  return;
71
71
  }
72
72
 
73
- const reply = event.target.closest('[data-reply-comment]');
74
- if (reply) {
75
- const region = reply.closest('[data-engagement-url]');
76
- const form = region?.querySelector('[data-comment-create]');
77
- if (!form) return;
78
- form.dataset.parentCommentId = reply.dataset.replyComment;
79
- const context = form.querySelector('[data-comment-reply-context]');
80
- if (context) {
81
- context.hidden = false;
82
- context.textContent = `Replying to ${reply.dataset.replyAuthor || 'comment'}.`;
83
- }
84
- const cancel = form.querySelector('[data-cancel-reply]');
85
- if (cancel) cancel.hidden = false;
86
- form.querySelector('textarea')?.focus();
87
- return;
88
- }
89
-
90
- const cancelReply = event.target.closest('[data-cancel-reply]');
91
- if (cancelReply) {
92
- clearReply(cancelReply.closest('[data-comment-create]'));
93
- return;
94
- }
95
-
96
- const edit = event.target.closest('[data-edit-comment]');
97
- if (edit) {
98
- showCommentEditor(edit);
99
- return;
100
- }
101
-
102
- const cancelEdit = event.target.closest('[data-cancel-comment-edit]');
103
- if (cancelEdit) {
104
- cancelEdit.closest('[data-comment-editor]')?.remove();
105
- return;
106
- }
107
-
108
- const remove = event.target.closest('[data-delete-comment]');
109
- if (remove) {
110
- const region = remove.closest('[data-engagement-url]');
111
- if (!region) return;
112
- await mutateEngagement(region, 'comment.delete', {
113
- articleId: region.dataset.articleId,
114
- commentId: remove.dataset.deleteComment
115
- });
116
- return;
117
- }
118
-
119
- const moreComments = event.target.closest('[data-comments-cursor]');
120
- if (moreComments) {
121
- const region = moreComments.closest('[data-engagement-url]');
122
- if (!region) return;
123
- moreComments.disabled = true;
124
- await refreshEngagement(region, moreComments.dataset.commentsCursor, true);
125
- return;
126
- }
127
-
128
73
  const share = event.target.closest('[data-copy-url]');
129
74
  if (share) {
130
75
  const value = share.dataset.copyUrl;
@@ -187,57 +132,6 @@ function engagementCounts(data) {
187
132
  ];
188
133
  }
189
134
 
190
- function commentList(items, currentUser) {
191
- const roots = document.createElement('ol');
192
- roots.className = 'gala-comments';
193
- const nodes = new Map();
194
- for (const comment of items) {
195
- const item = document.createElement('li');
196
- item.dataset.commentId = comment.commentId;
197
- const author = comment.author?.displayName ?? '[deleted]';
198
- item.append(textElement('strong', author));
199
- const body = textElement('p', comment.deleted ? '[deleted]' : comment.body);
200
- body.dataset.commentBody = '';
201
- item.append(body);
202
- if (!comment.deleted && currentUser) {
203
- const actions = document.createElement('div');
204
- actions.className = 'gala-comment-actions';
205
- const reply = textElement('button', 'Reply');
206
- reply.type = 'button';
207
- reply.dataset.replyComment = comment.commentId;
208
- reply.dataset.replyAuthor = author;
209
- actions.append(reply);
210
- if (comment.author?.userId === currentUser.id) {
211
- const edit = textElement('button', 'Edit');
212
- edit.type = 'button';
213
- edit.dataset.editComment = comment.commentId;
214
- const remove = textElement('button', 'Delete');
215
- remove.type = 'button';
216
- remove.dataset.deleteComment = comment.commentId;
217
- actions.append(edit, remove);
218
- }
219
- item.append(actions);
220
- }
221
- nodes.set(comment.commentId, item);
222
- }
223
- for (const comment of items) {
224
- const item = nodes.get(comment.commentId);
225
- const parent = comment.parentCommentId ? nodes.get(comment.parentCommentId) : null;
226
- if (!parent) {
227
- roots.append(item);
228
- continue;
229
- }
230
- let replies = parent.querySelector(':scope > ol');
231
- if (!replies) {
232
- replies = document.createElement('ol');
233
- replies.className = 'gala-comment-replies';
234
- parent.append(replies);
235
- }
236
- replies.append(item);
237
- }
238
- return roots;
239
- }
240
-
241
135
  function renderEngagement(region, payload, appendComments = false) {
242
136
  const data = payload?.data;
243
137
  if (!data || typeof data !== 'object') throw new TypeError('Engagement data is invalid');
@@ -267,60 +161,19 @@ function renderEngagement(region, payload, appendComments = false) {
267
161
  live.append(summary);
268
162
  }
269
163
 
270
- if (Array.isArray(data.comments?.items) && data.comments.items.length > 0) {
271
- live.append(commentList(data.comments.items, sessionUser));
272
- }
273
- live.querySelector('[data-comments-cursor]')?.remove();
274
- if (data.comments?.nextCursor) {
275
- const more = textElement('button', 'Load more comments');
276
- more.type = 'button';
277
- more.dataset.commentsCursor = data.comments.nextCursor;
278
- live.append(more);
279
- }
280
164
  region.querySelector('[data-engagement-snapshot]')?.remove();
281
165
  region.querySelector('.gala-engagement__placeholder')?.remove();
282
166
  }
283
167
 
284
- function clearReply(form) {
285
- if (!form) return;
286
- delete form.dataset.parentCommentId;
287
- const context = form.querySelector('[data-comment-reply-context]');
288
- if (context) {
289
- context.hidden = true;
290
- context.textContent = '';
291
- }
292
- const cancel = form.querySelector('[data-cancel-reply]');
293
- if (cancel) cancel.hidden = true;
294
- }
295
-
296
- function showCommentEditor(control) {
297
- const item = control.closest('[data-comment-id]');
298
- if (!item || item.querySelector('[data-comment-editor]')) return;
299
- const form = document.createElement('form');
300
- form.dataset.commentEditor = control.dataset.editComment;
301
- const textarea = document.createElement('textarea');
302
- textarea.name = 'body';
303
- textarea.maxLength = 5000;
304
- textarea.required = true;
305
- textarea.value = item.querySelector('[data-comment-body]')?.textContent ?? '';
306
- const save = textElement('button', 'Save');
307
- save.type = 'submit';
308
- const cancel = textElement('button', 'Cancel');
309
- cancel.type = 'button';
310
- cancel.dataset.cancelCommentEdit = '';
311
- form.append(textarea, save, cancel);
312
- item.append(form);
313
- textarea.focus();
314
- }
315
-
316
- async function refreshEngagement(region, commentsCursor = '', appendComments = false) {
168
+ async function refreshEngagement(region, commentsCursor = '', appendComments = false, fresh = false) {
317
169
  const status = region.querySelector('[data-engagement-status]');
318
170
  try {
319
171
  const requestUrl = new URL(region.dataset.engagementUrl);
320
172
  if (commentsCursor) requestUrl.searchParams.set('commentsCursor', commentsCursor);
321
173
  const response = await fetch(requestUrl, {
322
174
  headers: { Accept: 'application/json' },
323
- credentials: 'omit'
175
+ credentials: 'omit',
176
+ cache: fresh ? 'no-store' : 'default'
324
177
  });
325
178
  if (!response.ok) throw new Error(`Engagement returned HTTP ${response.status}`);
326
179
  const payload = await response.json();
@@ -358,52 +211,14 @@ function recordView(region) {
358
211
  }).catch(() => {});
359
212
  }
360
213
 
361
- // Asked at the moment of intent: the reader reaches for the comment box, and only then are they
362
- // asked who they are never as a wall in front of something they have not decided to do.
363
- document.addEventListener('focusin', (event) => {
214
+ /*
215
+ * The comments island asks for a sign-in when the reader reaches for something that needs one.
216
+ * Opening the window and replaying the interrupted intent stays here, because that is a property
217
+ * of the page, not of any one island.
218
+ */
219
+ document.addEventListener('gala-request-sign-in', (event) => {
364
220
  if (sessionUser || pendingIntent) return;
365
- const field = event.target.closest('[data-comment-create] textarea, [data-comment-editor] textarea');
366
- if (!field) return;
367
- const region = field.closest('[data-engagement-url]');
368
- const form = field.closest('[data-comment-create], [data-comment-editor]');
369
- if (!region || !form) return;
370
- requestSession({ kind: 'comment', region,
371
- selector: `${form.matches('[data-comment-create]') ? '[data-comment-create]' : '[data-comment-editor]'} textarea`,
372
- field });
373
- });
374
-
375
- document.addEventListener('submit', async (event) => {
376
- const create = event.target.closest('[data-comment-create]');
377
- const editor = event.target.closest('[data-comment-editor]');
378
- if (!create && !editor) return;
379
- event.preventDefault();
380
- const form = create ?? editor;
381
- const region = form.closest('[data-engagement-url]');
382
- const body = form.elements.body?.value;
383
- if (!region || typeof body !== 'string' || body.trim() === '') return;
384
- if (!sessionUser) {
385
- return requestSession({ kind: 'comment', region,
386
- selector: `${create ? '[data-comment-create]' : '[data-comment-editor]'} textarea`,
387
- field: form.querySelector('textarea') });
388
- }
389
- if (create) {
390
- const saved = await mutateEngagement(region, 'comment.create', {
391
- articleId: region.dataset.articleId,
392
- parentCommentId: create.dataset.parentCommentId || null,
393
- body
394
- });
395
- if (!saved) return;
396
- create.reset();
397
- clearReply(create);
398
- } else {
399
- const saved = await mutateEngagement(region, 'comment.edit', {
400
- articleId: region.dataset.articleId,
401
- commentId: editor.dataset.commentEditor,
402
- body
403
- });
404
- if (!saved) return;
405
- editor.remove();
406
- }
221
+ requestSession({ kind: event.detail?.kind ?? 'comment' });
407
222
  });
408
223
 
409
224
  let sessionUser = null;
@@ -556,7 +371,8 @@ async function mutateEngagement(region, operation, payload) {
556
371
  if (status) status.textContent = 'Saving…';
557
372
  await sendEngagementWrite(operation, payload);
558
373
  if (status) status.textContent = 'Saved.';
559
- await refreshEngagement(region);
374
+ // The reader's own write must be visible to them at once, so this one bypasses the cache.
375
+ await refreshEngagement(region, '', false, true);
560
376
  return true;
561
377
  } catch (error) {
562
378
  if (status) status.textContent = engagementErrorMessage(error.message);
@@ -1,13 +1,25 @@
1
1
  const MODES = ['system', 'light', 'dark'];
2
2
  const STORAGE_KEY = 'gala-color-mode';
3
3
 
4
+ /*
5
+ * The reader's own choice always wins. Absent one, the publication's `design.colorMode` decides
6
+ * what they see first — it is already on the element, server-rendered. This used to fall back to
7
+ * `system` unconditionally, which meant a writer could set a colour mode and no reader ever saw
8
+ * it: the setting existed everywhere except in the page.
9
+ */
10
+ function publicationDefault() {
11
+ const declared = document.documentElement.dataset.mode;
12
+ return MODES.includes(declared) ? declared : 'system';
13
+ }
14
+
4
15
  function storedMode() {
5
16
  try {
6
17
  const mode = localStorage.getItem(STORAGE_KEY);
7
- return MODES.includes(mode) ? mode : 'system';
18
+ if (MODES.includes(mode)) return mode;
8
19
  } catch {
9
- return 'system';
20
+ return publicationDefault();
10
21
  }
22
+ return publicationDefault();
11
23
  }
12
24
 
13
25
  function applyMode(mode) {