bearings 0.5.2 → 0.5.4

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.
@@ -2,22 +2,30 @@
2
2
  'use strict';
3
3
 
4
4
  var STATES = ['not-started', 'complete', 'failed', 'blocked', 'not-applicable'];
5
- var STATE_LABELS = {
6
- 'not-started': 'Not started',
7
- complete: 'Complete',
8
- failed: 'Failed',
9
- blocked: 'Blocked',
10
- 'not-applicable': 'Not applicable',
5
+ var STATE_META = {
6
+ 'not-started': { label: 'Not started', symbol: '○' },
7
+ complete: { label: 'Complete', symbol: '✓' },
8
+ failed: { label: 'Failed', symbol: '×' },
9
+ blocked: { label: 'Blocked', symbol: 'Ⅱ' },
10
+ 'not-applicable': { label: 'Not applicable', symbol: '—' },
11
11
  };
12
12
  var payload = window.BEARINGS_CHECKLISTS;
13
13
  var currentEntry = null;
14
- var currentStates = {};
14
+ var currentStates = Object.create(null);
15
15
  var storageAvailable = true;
16
16
  var refreshPending = false;
17
+ var taskViews = Object.create(null);
18
+ var taskSequence = [];
19
+ var groupViews = [];
20
+ var sectionViews = [];
21
+ var keyboardCursor = null;
22
+ var nextDomId = 0;
17
23
 
18
24
  var picker = document.getElementById('checklist-picker');
19
25
  var content = document.getElementById('content');
20
26
  var status = document.getElementById('status');
27
+ var actionStatus = document.getElementById('action-status');
28
+ var actionMenu = document.getElementById('action-menu');
21
29
  var warning = document.getElementById('storage-warning');
22
30
 
23
31
  function element(tag, className, text) {
@@ -27,34 +35,61 @@
27
35
  return node;
28
36
  }
29
37
 
30
- function setStatus(message) {
38
+ function setStatus(message, isError) {
31
39
  status.textContent = message;
40
+ status.dataset.tone = isError ? 'error' : 'info';
41
+ status.setAttribute('role', isError ? 'alert' : 'status');
42
+ }
43
+
44
+ function setActionStatus(message, isError) {
45
+ actionStatus.hidden = false;
46
+ actionStatus.textContent = message;
47
+ actionStatus.dataset.tone = isError ? 'error' : 'info';
48
+ actionStatus.setAttribute('role', isError ? 'alert' : 'status');
49
+ if (isError) actionMenu.open = true;
32
50
  }
33
51
 
34
52
  function reportStorageFailure() {
35
53
  storageAvailable = false;
36
54
  warning.hidden = false;
37
- warning.textContent = 'Browser storage is unavailable. Progress works in this tab only; use Export to keep a copy.';
55
+ warning.textContent = 'Browser storage is unavailable. Progress works in this tab only; use Export progress to keep a copy.';
38
56
  }
39
57
 
40
58
  function storageKey(entry) {
41
59
  return 'bearings-checklists:' + payload.repositoryId + ':' + entry.identity;
42
60
  }
43
61
 
44
- function executableIds(entry) {
45
- var ids = [];
46
- entry.checklist.sections.forEach(function (section) {
62
+ function hasOwnTaskContent(item) {
63
+ return Boolean(
64
+ item.instruction || item.warning || item.expectedResult || item.failureGuidance ||
65
+ (item.codeBlocks && item.codeBlocks.length) || (item.links && item.links.length)
66
+ );
67
+ }
68
+
69
+ function isGroupOnly(item) {
70
+ return Boolean(item.subItems && item.subItems.length && !hasOwnTaskContent(item));
71
+ }
72
+
73
+ function executableItems(entry) {
74
+ var items = [];
75
+ entry.checklist.sections.forEach(function (section, sectionIndex) {
47
76
  if (section.kind === 'info') return;
48
77
  section.items.forEach(function (item) {
49
- ids.push(item.id);
50
- (item.subItems || []).forEach(function (subItem) { ids.push(subItem.id); });
78
+ if (!isGroupOnly(item)) items.push({ id: item.id, title: item.title, sectionIndex: sectionIndex });
79
+ (item.subItems || []).forEach(function (subItem) {
80
+ items.push({ id: subItem.id, title: subItem.title, sectionIndex: sectionIndex });
81
+ });
51
82
  });
52
83
  });
53
- return ids;
84
+ return items;
85
+ }
86
+
87
+ function executableIds(entry) {
88
+ return executableItems(entry).map(function (item) { return item.id; });
54
89
  }
55
90
 
56
91
  function normalizedStates(entry, candidate) {
57
- var states = {};
92
+ var states = Object.create(null);
58
93
  executableIds(entry).forEach(function (id) {
59
94
  if (candidate && STATES.indexOf(candidate[id]) !== -1 && candidate[id] !== 'not-started') {
60
95
  states[id] = candidate[id];
@@ -64,14 +99,14 @@
64
99
  }
65
100
 
66
101
  function loadProgress(entry) {
67
- if (!storageAvailable) return {};
102
+ if (!storageAvailable) return Object.create(null);
68
103
  try {
69
104
  var saved = JSON.parse(localStorage.getItem(storageKey(entry)) || 'null');
70
- if (!saved || saved.contentHash !== entry.contentHash) return {};
105
+ if (!saved || saved.contentHash !== entry.contentHash) return Object.create(null);
71
106
  return normalizedStates(entry, saved.states);
72
107
  } catch (error) {
73
108
  reportStorageFailure();
74
- return {};
109
+ return Object.create(null);
75
110
  }
76
111
  }
77
112
 
@@ -92,21 +127,210 @@
92
127
  setStatus('Progress saved in this browser.');
93
128
  } catch (error) {
94
129
  reportStorageFailure();
95
- setStatus('Progress changed, but browser storage could not save it.');
130
+ setStatus('Progress changed, but browser storage could not save it.', true);
131
+ }
132
+ }
133
+
134
+ function itemState(id) {
135
+ return currentStates[id] || 'not-started';
136
+ }
137
+
138
+ function tally(ids) {
139
+ var counts = { complete: 0, failed: 0, blocked: 0, 'not-applicable': 0, 'not-started': 0 };
140
+ ids.forEach(function (id) { counts[itemState(id)] += 1; });
141
+ return counts;
142
+ }
143
+
144
+ function setBadge(badge, stateName, label) {
145
+ var meta = STATE_META[stateName] || STATE_META['not-started'];
146
+ badge.dataset.state = stateName;
147
+ badge.replaceChildren(
148
+ element('span', 'status-symbol', meta.symbol),
149
+ document.createTextNode(label || meta.label)
150
+ );
151
+ badge.firstChild.setAttribute('aria-hidden', 'true');
152
+ }
153
+
154
+ function setExpanded(view, expanded) {
155
+ view.body.hidden = !expanded;
156
+ view.toggle.setAttribute('aria-expanded', String(expanded));
157
+ view.toggle.textContent = expanded ? 'Hide details' : 'Show details';
158
+ }
159
+
160
+ function isUnresolved(id) {
161
+ var stateName = itemState(id);
162
+ return stateName !== 'complete' && stateName !== 'not-applicable';
163
+ }
164
+
165
+ function tasksInSection(sectionIndex) {
166
+ return taskSequence.filter(function (task) { return task.sectionIndex === sectionIndex; });
167
+ }
168
+
169
+ function preferredTaskInSection(sectionIndex) {
170
+ var tasks = tasksInSection(sectionIndex);
171
+ return tasks.find(function (task) { return isUnresolved(task.id); }) || tasks[0] || null;
172
+ }
173
+
174
+ function firstUnresolvedTask() {
175
+ return taskSequence.find(function (task) { return isUnresolved(task.id); }) || null;
176
+ }
177
+
178
+ function nextUnresolvedTaskAfter(id) {
179
+ var index = taskSequence.findIndex(function (task) { return task.id === id; });
180
+ return taskSequence.slice(index + 1).find(function (task) { return isUnresolved(task.id); }) || null;
181
+ }
182
+
183
+ function hasTextSelection() {
184
+ var selection = window.getSelection && window.getSelection();
185
+ return Boolean(selection && !selection.isCollapsed);
186
+ }
187
+
188
+ function isNativeShortcutTarget(target) {
189
+ if (!target || target.nodeType !== 1) return false;
190
+ return target.isContentEditable || Boolean(target.closest('input, select, textarea, button, a, summary, [contenteditable]:not([contenteditable="false"])'));
191
+ }
192
+
193
+ function setKeyboardCursor(sectionIndex, actionId, options) {
194
+ options = options || {};
195
+ if (!currentEntry || !sectionViews.length) {
196
+ keyboardCursor = null;
197
+ return;
198
+ }
199
+
200
+ var section = sectionViews.find(function (view) { return view.index === sectionIndex; }) || sectionViews[0];
201
+ var action = actionId && taskSequence.find(function (task) {
202
+ return task.id === actionId && task.sectionIndex === section.index;
203
+ });
204
+ var previous = keyboardCursor;
205
+ var changed = !previous || previous.sectionIndex !== section.index || previous.actionId !== (action ? action.id : null);
206
+ keyboardCursor = { sectionIndex: section.index, actionId: action ? action.id : null };
207
+ if (changed) {
208
+ var chooser = content.querySelector('.copy-chooser');
209
+ if (chooser) {
210
+ if (chooser.open) chooser.close();
211
+ chooser.remove();
212
+ }
96
213
  }
214
+
215
+ sectionViews.forEach(function (view) {
216
+ var current = view.index === keyboardCursor.sectionIndex;
217
+ view.element.dataset.keyboardCurrent = String(current);
218
+ if (current) view.link.setAttribute('aria-current', 'location');
219
+ else view.link.removeAttribute('aria-current');
220
+ });
221
+
222
+ Object.keys(taskViews).forEach(function (id) {
223
+ var view = taskViews[id];
224
+ var current = id === keyboardCursor.actionId;
225
+ view.article.dataset.keyboardCurrent = String(current);
226
+ view.focusMarker.hidden = !current;
227
+ if (current) view.article.setAttribute('aria-current', 'step');
228
+ else view.article.removeAttribute('aria-current');
229
+ if (!changed) return;
230
+ if (current || view.childIds.indexOf(keyboardCursor.actionId) !== -1) setExpanded(view, true);
231
+ else setExpanded(view, view.manualExpanded === undefined ? false : view.manualExpanded);
232
+ });
233
+
234
+ var target = action ? taskViews[action.id].article : section.element;
235
+ if (options.focus) {
236
+ try { target.focus({ preventScroll: true }); } catch (error) { target.focus(); }
237
+ }
238
+ if (options.scroll) target.scrollIntoView({ block: action ? 'center' : 'start' });
239
+ }
240
+
241
+ function setTaskVisual(view, stateName) {
242
+ view.article.dataset.state = stateName;
243
+ setBadge(view.badge, stateName);
244
+ view.buttons.forEach(function (button) {
245
+ var selected = button.dataset.stateValue === stateName;
246
+ button.dataset.selected = String(selected);
247
+ button.setAttribute('aria-pressed', String(selected));
248
+ });
249
+ if (view.failure) view.failure.hidden = stateName !== 'failed' && stateName !== 'blocked';
250
+ }
251
+
252
+ function updateGroupViews(nextUnresolvedId) {
253
+ groupViews.forEach(function (view) {
254
+ var counts = tally(view.ids);
255
+ var stateName = 'not-started';
256
+ if (counts.failed) stateName = 'failed';
257
+ else if (counts.blocked) stateName = 'blocked';
258
+ else if (counts.complete + counts['not-applicable'] === view.ids.length) {
259
+ stateName = counts.complete ? 'complete' : 'not-applicable';
260
+ }
261
+ var label = counts.complete + ' / ' + view.ids.length + ' complete';
262
+ if (counts.failed) label += ' · ' + counts.failed + ' failed';
263
+ if (counts.blocked) label += ' · ' + counts.blocked + ' blocked';
264
+ setBadge(view.badge, stateName, label);
265
+ view.article.dataset.state = stateName;
266
+ view.article.dataset.containsNext = String(view.ids.indexOf(nextUnresolvedId) !== -1);
267
+ if (view.counter) view.counter.textContent = label;
268
+ });
97
269
  }
98
270
 
99
- function updateProgress() {
100
- var ids = currentEntry ? executableIds(currentEntry) : [];
101
- var resolved = ids.filter(function (id) {
102
- return currentStates[id] && currentStates[id] !== 'not-started';
103
- }).length;
104
- document.getElementById('progress-copy').textContent = resolved + ' / ' + ids.length + ' resolved';
105
- document.getElementById('progress-fill').style.width = (ids.length ? resolved / ids.length * 100 : 0) + '%';
271
+ function updateDashboard() {
272
+ var entries = currentEntry ? executableItems(currentEntry) : [];
273
+ var ids = entries.map(function (entry) { return entry.id; });
274
+ var counts = tally(ids);
275
+ var total = ids.length;
276
+ var nextUnresolved = entries.find(function (entry) { return isUnresolved(entry.id); });
277
+ var nextUnresolvedIndex = nextUnresolved ? entries.indexOf(nextUnresolved) : -1;
278
+ var followingUnresolved = nextUnresolvedIndex === -1 ? null : entries.slice(nextUnresolvedIndex + 1).find(function (entry) { return isUnresolved(entry.id); });
279
+
280
+ document.getElementById('progress-copy').textContent = counts.complete + ' / ' + total + ' complete';
281
+ document.getElementById('failed-count').textContent = counts.failed;
282
+ document.getElementById('blocked-count').textContent = counts.blocked;
283
+ document.getElementById('na-count').textContent = counts['not-applicable'];
284
+ var track = document.getElementById('progress-track');
285
+ track.max = total || 1;
286
+ track.value = counts.complete;
287
+ track.setAttribute('aria-valuetext', counts.complete + ' of ' + total + ' complete; ' + counts.failed + ' failed; ' + counts.blocked + ' blocked; ' + counts['not-applicable'] + ' not applicable');
288
+
289
+ var currentLink = document.getElementById('current-task-link');
290
+ var nextCopy = document.getElementById('next-task-copy');
291
+ if (nextUnresolved && taskViews[nextUnresolved.id]) {
292
+ currentLink.textContent = nextUnresolved.id + ' ' + nextUnresolved.title;
293
+ currentLink.title = currentLink.textContent;
294
+ currentLink.href = '#' + taskViews[nextUnresolved.id].domId;
295
+ nextCopy.textContent = followingUnresolved ? 'Next: ' + followingUnresolved.id + ' ' + followingUnresolved.title : 'Next: finish this task';
296
+ } else if (currentEntry && total) {
297
+ currentLink.textContent = 'Checklist complete';
298
+ currentLink.removeAttribute('title');
299
+ currentLink.removeAttribute('href');
300
+ nextCopy.textContent = 'No open tasks';
301
+ } else {
302
+ currentLink.textContent = 'Choose a checklist';
303
+ currentLink.removeAttribute('title');
304
+ currentLink.href = '#content';
305
+ nextCopy.textContent = 'Next: —';
306
+ }
307
+
308
+ Object.keys(taskViews).forEach(function (id) {
309
+ var view = taskViews[id];
310
+ var isNextUnresolved = Boolean(nextUnresolved && id === nextUnresolved.id);
311
+ var containsNext = Boolean(nextUnresolved && view.childIds.indexOf(nextUnresolved.id) !== -1);
312
+ view.article.dataset.nextUnresolved = String(isNextUnresolved);
313
+ view.article.dataset.containsNext = String(containsNext);
314
+ view.marker.hidden = !isNextUnresolved;
315
+ });
316
+
317
+ updateGroupViews(nextUnresolved && nextUnresolved.id);
318
+ updateSubstepCounters();
319
+ sectionViews.forEach(function (view) {
320
+ if (view.kind === 'info') {
321
+ view.count.textContent = 'Info';
322
+ } else {
323
+ var sectionCounts = tally(view.ids);
324
+ var sectionLabel = sectionCounts.complete + '/' + view.ids.length;
325
+ if (sectionCounts.failed) sectionLabel += ' · ' + sectionCounts.failed + ' failed';
326
+ else if (sectionCounts.blocked) sectionLabel += ' · ' + sectionCounts.blocked + ' blocked';
327
+ view.count.textContent = sectionLabel;
328
+ }
329
+ });
106
330
  }
107
331
 
108
332
  function textBlock(className, label, value) {
109
- var block = element('div', className);
333
+ var block = element('div', 'step-block ' + className);
110
334
  block.append(element('span', 'field-label', label), document.createTextNode(value));
111
335
  return block;
112
336
  }
@@ -127,104 +351,574 @@
127
351
  return item;
128
352
  }
129
353
 
130
- function stateControl(item, article) {
131
- var label = element('label', 'state-label', 'State');
132
- var select = element('select', 'state-select');
133
- select.setAttribute('aria-label', 'State for ' + item.id + ': ' + item.title);
134
- STATES.forEach(function (stateName) {
135
- var option = element('option', '', STATE_LABELS[stateName]);
136
- option.value = stateName;
137
- select.append(option);
354
+ function fallbackCopy(value) {
355
+ var input = element('textarea');
356
+ input.value = value;
357
+ input.className = 'copy-source';
358
+ input.setAttribute('readonly', '');
359
+ document.body.append(input);
360
+ input.select();
361
+ var copied = false;
362
+ try { copied = document.execCommand('copy'); } catch (error) { copied = false; }
363
+ input.remove();
364
+ return copied;
365
+ }
366
+
367
+ function copyCode(value, button) {
368
+ function copied() {
369
+ button.textContent = 'Copied';
370
+ setStatus('Command copied.');
371
+ }
372
+ function fallback() {
373
+ if (fallbackCopy(value)) copied();
374
+ else setStatus('Copy failed. Select the command and copy it manually.', true);
375
+ }
376
+ button.textContent = 'Copying…';
377
+ if (navigator.clipboard && navigator.clipboard.writeText) {
378
+ navigator.clipboard.writeText(value).then(copied, fallback);
379
+ } else fallback();
380
+ }
381
+
382
+ function codeBlock(block) {
383
+ var wrapper = element('div', 'code-block');
384
+ var heading = element('div', 'code-heading');
385
+ var copy = element('button', 'copy-button', 'Copy');
386
+ copy.type = 'button';
387
+ copy.setAttribute('aria-label', 'Copy ' + block.language + ' command');
388
+ copy.addEventListener('click', function () { copyCode(block.content, copy); });
389
+ heading.append(element('span', 'language', block.language), copy);
390
+ var pre = element('pre');
391
+ pre.append(element('code', '', block.content));
392
+ wrapper.append(heading, pre);
393
+ return wrapper;
394
+ }
395
+
396
+ function appendItemContent(item, body, kind) {
397
+ var failure = null;
398
+ if (item.warning) body.append(textBlock('warning', 'Warning', item.warning));
399
+ if (item.instruction) body.append(textBlock('instruction', kind === 'info' ? 'Information' : 'Do', item.instruction));
400
+ (item.codeBlocks || []).forEach(function (block) { body.append(codeBlock(block)); });
401
+ if (item.expectedResult) body.append(textBlock('expected', 'Verify', item.expectedResult));
402
+ if (item.failureGuidance) {
403
+ failure = textBlock('failure', 'Recovery', item.failureGuidance);
404
+ body.append(failure);
405
+ }
406
+ if (item.links && item.links.length) {
407
+ var resources = element('div', 'resources');
408
+ var links = element('ul', 'links');
409
+ item.links.forEach(function (link) { links.append(safeLink(link)); });
410
+ resources.append(element('span', 'field-label', 'Resources'), links);
411
+ body.append(resources);
412
+ }
413
+ return failure;
414
+ }
415
+
416
+ function stateButton(item, stateName, label, className, menu) {
417
+ var button = element('button', 'state-action' + (className ? ' ' + className : ''), label);
418
+ button.type = 'button';
419
+ button.dataset.stateValue = stateName;
420
+ button.setAttribute('aria-label', label + ' for ' + item.id + ': ' + item.title);
421
+ button.addEventListener('click', function () {
422
+ if (menu) menu.open = false;
423
+ changeItemState(item.id, stateName);
138
424
  });
139
- select.value = currentStates[item.id] || 'not-started';
140
- article.dataset.state = select.value;
141
- select.addEventListener('change', function () {
142
- if (select.value === 'not-started') delete currentStates[item.id];
143
- else currentStates[item.id] = select.value;
144
- article.dataset.state = select.value;
145
- saveProgress();
146
- updateProgress();
425
+ return button;
426
+ }
427
+
428
+ function stateActions(item) {
429
+ var actions = element('div', 'state-actions');
430
+ var buttons = [];
431
+ var complete = stateButton(item, 'complete', 'Mark complete', 'primary-action');
432
+ var blocked = stateButton(item, 'blocked', 'Blocked');
433
+ var failed = stateButton(item, 'failed', 'Failed');
434
+ var more = element('details', 'state-more');
435
+ var moreSummary = element('summary', '', 'More');
436
+ var menu = element('div', 'state-more-menu');
437
+ var notApplicable = stateButton(item, 'not-applicable', 'Not applicable', '', more);
438
+ var notStarted = stateButton(item, 'not-started', 'Mark not started', '', more);
439
+ buttons.push(complete, blocked, failed, notApplicable, notStarted);
440
+ menu.append(notApplicable, notStarted);
441
+ more.append(moreSummary, menu);
442
+ actions.append(complete, blocked, failed, more);
443
+ return { element: actions, buttons: buttons };
444
+ }
445
+
446
+ function scrollToNextTask(id) {
447
+ var index = taskSequence.findIndex(function (task) { return task.id === id; });
448
+ var next = taskSequence.slice(index + 1).find(function (task) {
449
+ var stateName = itemState(task.id);
450
+ return stateName !== 'complete' && stateName !== 'not-applicable';
147
451
  });
148
- label.append(select);
149
- return label;
452
+ if (next && taskViews[next.id]) taskViews[next.id].article.scrollIntoView({ block: 'center' });
150
453
  }
151
454
 
152
- function renderItem(item, kind, isSubItem) {
153
- var article = element('article', 'item' + (kind === 'info' ? ' info' : ''));
154
- var head = element('div', 'item-head');
155
- var headingWrap = element('div', 'item-heading');
455
+ function changeItemState(id, stateName) {
456
+ var wasComplete = itemState(id) === 'complete';
457
+ if (stateName === 'not-started') delete currentStates[id];
458
+ else currentStates[id] = stateName;
459
+ var view = taskViews[id];
460
+ view.manualExpanded = stateName === 'complete' || stateName === 'not-applicable' ? false : true;
461
+ var focusWasInBody = view.body.contains(document.activeElement);
462
+ setExpanded(view, view.manualExpanded);
463
+ setTaskVisual(view, stateName);
464
+ saveProgress();
465
+ updateDashboard();
466
+ if (stateName === 'complete' && !wasComplete) scrollToNextTask(id);
467
+ if (focusWasInBody && view.body.hidden) {
468
+ try { view.article.focus({ preventScroll: true }); } catch (error) { view.article.focus(); }
469
+ }
470
+ setStatus(id + ': ' + STATE_META[stateName].label + (storageAvailable ? '.' : '. Progress is only in this tab.'), !storageAvailable);
471
+ }
472
+
473
+ function itemHeading(item, isSubItem, label) {
474
+ var wrap = element('div', 'item-heading');
475
+ wrap.append(element('span', 'hierarchy-label', label));
156
476
  var heading = element(isSubItem ? 'h4' : 'h3');
157
477
  heading.append(element('span', 'item-id', item.id), document.createTextNode(item.title));
158
478
  if (item.optional) heading.append(element('span', 'optional', 'Optional'));
159
- headingWrap.append(heading);
160
- head.append(headingWrap);
161
- if (kind === 'checklist') head.append(stateControl(item, article));
162
- article.append(head);
163
-
164
- if (item.warning) article.append(textBlock('warning', 'Warning', item.warning));
165
- if (item.instruction) article.append(element('p', 'instruction', item.instruction));
166
- (item.codeBlocks || []).forEach(function (block) {
167
- var pre = element('pre');
168
- pre.append(element('span', 'language', block.language), element('code', '', block.content));
169
- article.append(pre);
170
- });
171
- if (item.expectedResult) article.append(textBlock('expected', 'Expected result', item.expectedResult));
172
- if (item.failureGuidance) article.append(textBlock('failure', 'If this fails', item.failureGuidance));
173
- if (item.links && item.links.length) {
174
- var links = element('ul', 'links');
175
- item.links.forEach(function (link) { links.append(safeLink(link)); });
176
- article.append(links);
479
+ wrap.append(heading);
480
+ return wrap;
481
+ }
482
+
483
+ function renderSubItems(item, target, kind, sectionIndex) {
484
+ if (!item.subItems || !item.subItems.length) return { ids: [], counter: null };
485
+ var wrapper = element('div', 'substeps');
486
+ var heading = element('div', 'substeps-heading');
487
+ var counter = element('span', '', '0 / ' + item.subItems.length + ' complete');
488
+ heading.append(element('span', '', 'Substeps'), counter);
489
+ var items = element('div', 'sub-items');
490
+ item.subItems.forEach(function (subItem) { items.append(renderItem(subItem, kind, true, sectionIndex)); });
491
+ wrapper.append(heading, items);
492
+ target.append(wrapper);
493
+ return { ids: item.subItems.map(function (subItem) { return subItem.id; }), counter: counter };
494
+ }
495
+
496
+ function renderItem(item, kind, isSubItem, sectionIndex) {
497
+ var groupOnly = kind === 'checklist' && isGroupOnly(item);
498
+ var article = element('article', 'item' + (kind === 'info' ? ' info' : '') + (groupOnly ? ' task-group' : ''));
499
+ article.id = 'task-' + (++nextDomId);
500
+ var head = element('div', 'item-head');
501
+
502
+ if (kind === 'info') {
503
+ head.append(itemHeading(item, isSubItem, 'Reference'));
504
+ var infoBadge = element('span', 'status-badge');
505
+ setBadge(infoBadge, 'not-applicable', 'Reference');
506
+ head.append(infoBadge);
507
+ var infoBody = element('div', 'item-body');
508
+ appendItemContent(item, infoBody, kind);
509
+ article.append(head, infoBody);
510
+ renderSubItems(item, infoBody, kind, sectionIndex);
511
+ article.addEventListener('click', function (event) {
512
+ setKeyboardCursor(sectionIndex, null, { focus: !isNativeShortcutTarget(event.target) && !hasTextSelection() });
513
+ });
514
+ article.addEventListener('focusin', function () { setKeyboardCursor(sectionIndex, null); });
515
+ return article;
177
516
  }
178
- if (item.subItems && item.subItems.length) {
179
- var subItems = element('div', 'sub-items');
180
- item.subItems.forEach(function (subItem) { subItems.append(renderItem(subItem, kind, true)); });
181
- article.append(subItems);
517
+
518
+ if (groupOnly) {
519
+ head.append(itemHeading(item, false, 'Task group'));
520
+ var groupBadge = element('span', 'status-badge');
521
+ head.append(groupBadge);
522
+ article.append(head);
523
+ var groupChildren = renderSubItems(item, article, kind, sectionIndex);
524
+ groupViews.push({ article: article, badge: groupBadge, ids: groupChildren.ids, counter: groupChildren.counter });
525
+ article.addEventListener('click', function (event) {
526
+ if (!event.target.closest('[data-keyboard-task]')) {
527
+ setKeyboardCursor(sectionIndex, null, { focus: !isNativeShortcutTarget(event.target) && !hasTextSelection() });
528
+ }
529
+ });
530
+ article.addEventListener('focusin', function (event) {
531
+ if (!event.target.closest('[data-keyboard-task]')) setKeyboardCursor(sectionIndex, null);
532
+ });
533
+ return article;
182
534
  }
535
+
536
+ var hierarchy = isSubItem ? 'Substep' : (item.subItems && item.subItems.length ? 'Parent task' : 'Task');
537
+ var headingWrap = itemHeading(item, isSubItem, hierarchy);
538
+ var marker = element('span', 'next-unresolved-marker', 'Next unresolved');
539
+ marker.hidden = true;
540
+ var focusMarker = element('span', 'focused-task-marker', 'Focused action');
541
+ focusMarker.hidden = true;
542
+ headingWrap.append(marker, focusMarker);
543
+ var badge = element('span', 'status-badge');
544
+ var toggle = element('button', 'item-toggle', 'Show details');
545
+ toggle.type = 'button';
546
+ var body = element('div', 'item-body');
547
+ var view = {
548
+ article: article,
549
+ badge: badge,
550
+ body: body,
551
+ toggle: toggle,
552
+ marker: marker,
553
+ focusMarker: focusMarker,
554
+ buttons: [],
555
+ copyButtons: [],
556
+ failure: null,
557
+ childIds: [],
558
+ substepCounter: null,
559
+ manualExpanded: undefined,
560
+ domId: article.id,
561
+ };
562
+ taskViews[item.id] = view;
563
+ taskSequence.push({ id: item.id, title: item.title, sectionIndex: sectionIndex });
564
+ article.dataset.keyboardTask = item.id;
565
+ article.tabIndex = -1;
566
+ toggle.setAttribute('aria-controls', article.id + '-body');
567
+ body.id = article.id + '-body';
568
+ toggle.addEventListener('click', function () {
569
+ view.manualExpanded = body.hidden;
570
+ setExpanded(view, view.manualExpanded);
571
+ });
572
+ article.addEventListener('focusin', function (event) {
573
+ if (event.target.closest('[data-keyboard-task]') !== article) return;
574
+ setKeyboardCursor(sectionIndex, item.id);
575
+ });
576
+ article.addEventListener('click', function (event) {
577
+ if (!event.isTrusted || event.target.closest('[data-keyboard-task]') !== article) return;
578
+ var focusTask = !isNativeShortcutTarget(event.target) && !hasTextSelection();
579
+ setKeyboardCursor(sectionIndex, item.id, { focus: focusTask });
580
+ });
581
+ head.append(headingWrap, badge, toggle);
582
+ article.append(head, body);
583
+ view.failure = appendItemContent(item, body, kind);
584
+ view.copyButtons = Array.prototype.slice.call(body.querySelectorAll('.copy-button'));
585
+ var children = renderSubItems(item, body, kind, sectionIndex);
586
+ view.childIds = children.ids;
587
+ view.substepCounter = children.counter;
588
+ var actions = stateActions(item);
589
+ view.buttons = actions.buttons;
590
+ body.append(actions.element);
591
+ setTaskVisual(view, itemState(item.id));
183
592
  return article;
184
593
  }
185
594
 
595
+ function updateSubstepCounters() {
596
+ Object.keys(taskViews).forEach(function (id) {
597
+ var view = taskViews[id];
598
+ if (!view.substepCounter) return;
599
+ var counts = tally(view.childIds);
600
+ var label = counts.complete + ' / ' + view.childIds.length + ' complete';
601
+ if (counts.failed) label += ' · ' + counts.failed + ' failed';
602
+ else if (counts.blocked) label += ' · ' + counts.blocked + ' blocked';
603
+ view.substepCounter.textContent = label;
604
+ });
605
+ }
606
+
607
+ function moveActionCursor(delta) {
608
+ if (!keyboardCursor) return false;
609
+ var tasks = tasksInSection(keyboardCursor.sectionIndex);
610
+ if (!tasks.length) return false;
611
+ var index = tasks.findIndex(function (task) { return task.id === keyboardCursor.actionId; });
612
+ var target = index === -1 ? tasks[delta > 0 ? 0 : tasks.length - 1] : tasks[index + delta];
613
+ if (!target) return false;
614
+ setKeyboardCursor(target.sectionIndex, target.id, { focus: true, scroll: true });
615
+ return true;
616
+ }
617
+
618
+ function moveSectionCursor(delta) {
619
+ if (!keyboardCursor) return false;
620
+ var position = sectionViews.findIndex(function (view) { return view.index === keyboardCursor.sectionIndex; });
621
+ var section = sectionViews[position + delta];
622
+ if (!section) return false;
623
+ var target = section.kind === 'checklist' ? preferredTaskInSection(section.index) : null;
624
+ setKeyboardCursor(section.index, target && target.id, { focus: true, scroll: true });
625
+ return true;
626
+ }
627
+
628
+ function changeCursorState(stateName, advance) {
629
+ if (!keyboardCursor || !keyboardCursor.actionId) return false;
630
+ var changedTask = taskSequence.find(function (task) { return task.id === keyboardCursor.actionId; });
631
+ if (!changedTask) return false;
632
+ changeItemState(changedTask.id, stateName);
633
+ var target = advance && nextUnresolvedTaskAfter(changedTask.id);
634
+ target = target || changedTask;
635
+ setKeyboardCursor(target.sectionIndex, target.id, { focus: true, scroll: Boolean(advance && target !== changedTask) });
636
+ return true;
637
+ }
638
+
639
+ function toggleCursorDetails() {
640
+ if (!keyboardCursor || !keyboardCursor.actionId) return false;
641
+ var view = taskViews[keyboardCursor.actionId];
642
+ if (!view) return false;
643
+ view.manualExpanded = view.body.hidden;
644
+ setExpanded(view, view.manualExpanded);
645
+ return true;
646
+ }
647
+
648
+ function dismissCopyChooser(message, restoreFocus) {
649
+ var chooser = content.querySelector('.copy-chooser');
650
+ if (!chooser) return false;
651
+ if (chooser.open) chooser.close();
652
+ chooser.remove();
653
+ if (restoreFocus && keyboardCursor) {
654
+ setKeyboardCursor(keyboardCursor.sectionIndex, keyboardCursor.actionId, { focus: true });
655
+ }
656
+ if (message) setStatus(message);
657
+ return true;
658
+ }
659
+
660
+ function showCopyChooser(buttons, action, section) {
661
+ dismissCopyChooser();
662
+ var chooser = element('dialog', 'copy-chooser');
663
+ var title = element('h2', 'copy-chooser-title', 'Copy which command?');
664
+ var hintText = element('p', 'copy-chooser-hint');
665
+ var choices = element('div', 'copy-choices');
666
+ var actions = element('div', 'copy-chooser-actions');
667
+ var cancel = element('button', '', 'Cancel');
668
+ var titleId = (action ? action.article.id : section.element.id) + '-copy-chooser-title';
669
+ var hintId = titleId + '-hint';
670
+ title.id = titleId;
671
+ hintText.id = hintId;
672
+ chooser.setAttribute('aria-labelledby', titleId);
673
+ chooser.setAttribute('aria-describedby', hintId);
674
+ cancel.type = 'button';
675
+ cancel.addEventListener('click', function () { dismissCopyChooser('Copy choice canceled.', true); });
676
+ actions.append(cancel);
677
+
678
+ buttons.forEach(function (button, index) {
679
+ var code = button.closest('.code-block').querySelector('code');
680
+ var command = code ? code.textContent.trim() : 'Command ' + (index + 1);
681
+ var preview = command.split('\n')[0];
682
+ var choice = element('button', 'copy-choice');
683
+ choice.type = 'button';
684
+ choice.dataset.copyChoice = String(index);
685
+ choice.setAttribute('aria-label', 'Copy command ' + (index + 1) + ': ' + command);
686
+ if (index < 9) choice.setAttribute('aria-keyshortcuts', String(index + 1));
687
+ choice.append(
688
+ element('span', 'copy-choice-key', String(index + 1)),
689
+ element('code', 'copy-choice-command', preview),
690
+ );
691
+ choice.addEventListener('click', function () {
692
+ dismissCopyChooser('', true);
693
+ button.click();
694
+ });
695
+ choices.append(choice);
696
+ });
697
+
698
+ var directCount = Math.min(buttons.length, 9);
699
+ var directKeys = directCount === 2 ? '1 or 2' : '1 through ' + directCount;
700
+ var hint = 'Press ' + directKeys + ' to copy; use j/k or Up/Down and Enter, or Escape to cancel.';
701
+ hintText.textContent = hint;
702
+ chooser.append(title, hintText, choices, actions);
703
+ chooser.addEventListener('cancel', function (event) {
704
+ event.preventDefault();
705
+ dismissCopyChooser('Copy choice canceled.', true);
706
+ });
707
+ content.append(chooser);
708
+ chooser.showModal();
709
+ setStatus(buttons.length + ' commands available. ' + hint);
710
+ try { choices.firstChild.focus({ preventScroll: true }); } catch (error) { choices.firstChild.focus(); }
711
+ }
712
+
713
+ function copyKeyboardTarget() {
714
+ if (!keyboardCursor) return false;
715
+ var section = sectionViews.find(function (view) { return view.index === keyboardCursor.sectionIndex; });
716
+ var action = keyboardCursor.actionId && taskViews[keyboardCursor.actionId];
717
+ var buttons = action
718
+ ? action.copyButtons
719
+ : (section ? Array.prototype.slice.call(section.element.querySelectorAll('.copy-button')) : []);
720
+ if (buttons.length === 1) {
721
+ buttons[0].click();
722
+ return true;
723
+ }
724
+ if (buttons.length > 1) showCopyChooser(buttons, action, section);
725
+ else setStatus('No copyable item in the focused ' + (action ? 'action.' : 'section.'));
726
+ return true;
727
+ }
728
+
729
+ function handleCopyChooserShortcut(event, target) {
730
+ var chooser = content.querySelector('.copy-chooser');
731
+ if (!chooser) return false;
732
+ var choices = Array.prototype.slice.call(chooser.querySelectorAll('.copy-choice'));
733
+ if (event.key === 'Escape') return dismissCopyChooser('Copy choice canceled.', true);
734
+ if (event.key >= '1' && event.key <= '9') {
735
+ if (!event.repeat && choices[Number(event.key) - 1]) choices[Number(event.key) - 1].click();
736
+ return true;
737
+ }
738
+ if (event.key === 'j' || event.key === 'k' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
739
+ var current = choices.indexOf(target.closest('.copy-choice'));
740
+ var delta = event.key === 'j' || event.key === 'ArrowDown' ? 1 : -1;
741
+ var next = current === -1
742
+ ? (delta > 0 ? 0 : choices.length - 1)
743
+ : (current + delta + choices.length) % choices.length;
744
+ choices[next].focus();
745
+ setStatus('Command ' + (next + 1) + ' of ' + choices.length + ' selected. Press Enter to copy or Escape to cancel.');
746
+ return true;
747
+ }
748
+ if (event.key === 'Enter' && target.closest('.copy-choice')) {
749
+ if (!event.repeat) target.closest('.copy-choice').click();
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+
755
+ function resetProgress() {
756
+ if (!currentEntry || !window.confirm('Clear progress for this checklist?')) return false;
757
+ currentStates = Object.create(null);
758
+ saveProgress();
759
+ renderChecklist(currentEntry, currentStates);
760
+ setActionStatus('Progress reset.');
761
+ actionMenu.open = false;
762
+ return true;
763
+ }
764
+
765
+ function handleEscape(target) {
766
+ var taskArticle = target && target.closest && target.closest('[data-keyboard-task]');
767
+ var details = target && target.closest && target.closest('details[open]');
768
+ if (!details && taskArticle) details = taskArticle.querySelector('details[open]');
769
+ if (!details && actionMenu.open) details = actionMenu;
770
+ if (details) details.open = false;
771
+
772
+ if (keyboardCursor && (details || (content.contains(target) && isNativeShortcutTarget(target)))) {
773
+ setKeyboardCursor(keyboardCursor.sectionIndex, keyboardCursor.actionId, { focus: true });
774
+ return true;
775
+ }
776
+ if (details) {
777
+ var summary = details.querySelector('summary');
778
+ if (summary) summary.focus();
779
+ return true;
780
+ }
781
+ var nextUnresolved = keyboardCursor && content.contains(target) && firstUnresolvedTask();
782
+ if (nextUnresolved) {
783
+ setKeyboardCursor(nextUnresolved.sectionIndex, nextUnresolved.id, { focus: true, scroll: true });
784
+ return true;
785
+ }
786
+ return false;
787
+ }
788
+
789
+ function handleKeyboardShortcut(event) {
790
+ if (event.defaultPrevented || event.isComposing || event.keyCode === 229) return;
791
+ if (event.ctrlKey || event.metaKey || event.altKey || hasTextSelection()) return;
792
+ var target = event.target && event.target.nodeType === 1 ? event.target : document.activeElement;
793
+ if (content.querySelector('.copy-chooser')) {
794
+ if (handleCopyChooserShortcut(event, target)) event.preventDefault();
795
+ return;
796
+ }
797
+ if (event.key === 'Escape') {
798
+ if (handleEscape(target)) event.preventDefault();
799
+ return;
800
+ }
801
+ if (!currentEntry || !keyboardCursor || !content.contains(document.activeElement) || isNativeShortcutTarget(target)) return;
802
+
803
+ var stateByKey = {
804
+ 'x': 'complete',
805
+ 'f': 'failed',
806
+ 'b': 'blocked',
807
+ '-': 'not-applicable',
808
+ 'u': 'not-started',
809
+ };
810
+ if ((stateByKey[event.key] || event.key === 'R' || event.key === 'y') && event.repeat) return;
811
+
812
+ var handled = false;
813
+ if (event.key === 'j') handled = moveActionCursor(1);
814
+ else if (event.key === 'k') handled = moveActionCursor(-1);
815
+ else if (event.key === ']') handled = moveSectionCursor(1);
816
+ else if (event.key === '[') handled = moveSectionCursor(-1);
817
+ else if (event.key === 'y') handled = copyKeyboardTarget();
818
+ else if (event.key === 'Enter') handled = toggleCursorDetails();
819
+ else if (event.key === 'R') handled = resetProgress();
820
+ else if (stateByKey[event.key]) handled = changeCursorState(stateByKey[event.key], event.key === 'x' || event.key === '-');
821
+ if (handled) event.preventDefault();
822
+ }
823
+
186
824
  function renderChecklist(entry, inMemoryStates) {
187
825
  currentEntry = entry;
188
826
  currentStates = inMemoryStates === undefined ? loadProgress(entry) : normalizedStates(entry, inMemoryStates);
827
+ taskViews = Object.create(null);
828
+ taskSequence = [];
829
+ groupViews = [];
830
+ sectionViews = [];
831
+ keyboardCursor = null;
832
+ nextDomId = 0;
189
833
  var checklist = entry.checklist;
190
834
  document.title = checklist.title + ' - Checklist';
191
835
  document.getElementById('title').textContent = checklist.title;
192
836
  document.getElementById('summary').textContent = checklist.summary;
193
837
  content.className = '';
194
838
  content.replaceChildren();
195
- if (checklist.intro) content.append(element('p', 'intro', checklist.intro));
196
- if (checklist.callout) content.append(element('div', 'callout', checklist.callout));
197
- if (checklist.conventions) content.append(element('div', 'conventions', checklist.conventions));
198
839
 
199
- checklist.sections.forEach(function (section) {
200
- var sectionElement = element('section');
840
+ var shell = element('div', 'checklist-shell');
841
+ var navigation = element('nav', 'section-nav');
842
+ navigation.setAttribute('aria-label', 'Checklist sections');
843
+ navigation.append(element('p', 'section-nav-title field-label', 'Sections'));
844
+ var navigationList = element('ol', 'section-nav-list');
845
+ navigation.append(navigationList);
846
+ var checklistBody = element('div', 'checklist-body');
847
+ if (checklist.intro) checklistBody.append(element('p', 'intro', checklist.intro));
848
+ if (checklist.callout) checklistBody.append(textBlock('callout', 'Important', checklist.callout));
849
+ if (checklist.conventions) checklistBody.append(textBlock('conventions', 'Conventions', checklist.conventions));
850
+
851
+ checklist.sections.forEach(function (section, sectionIndex) {
852
+ var sequenceStart = taskSequence.length;
853
+ var sectionElement = element('section', 'checklist-section');
854
+ sectionElement.id = 'section-' + (sectionIndex + 1);
855
+ sectionElement.tabIndex = -1;
201
856
  var heading = element('div', 'section-heading');
202
857
  heading.append(element('span', 'section-number', section.number), element('h2', '', section.title));
203
858
  sectionElement.append(heading);
204
859
  if (section.intro) sectionElement.append(element('p', 'section-intro', section.intro));
205
860
  var items = element('div', 'items');
206
- section.items.forEach(function (item) { items.append(renderItem(item, section.kind, false)); });
861
+ section.items.forEach(function (item) { items.append(renderItem(item, section.kind, false, sectionIndex)); });
207
862
  sectionElement.append(items);
208
- content.append(sectionElement);
863
+ checklistBody.append(sectionElement);
864
+
865
+ var navItem = element('li');
866
+ var navLink = element('a', 'section-nav-link');
867
+ navLink.href = '#' + sectionElement.id;
868
+ var navCount = element('span', 'nav-count');
869
+ navLink.append(
870
+ element('span', 'nav-number', section.number),
871
+ element('span', 'nav-title', section.title),
872
+ navCount
873
+ );
874
+ navItem.append(navLink);
875
+ navigationList.append(navItem);
876
+ var sectionView = {
877
+ index: sectionIndex,
878
+ kind: section.kind,
879
+ ids: taskSequence.slice(sequenceStart).map(function (task) { return task.id; }),
880
+ element: sectionElement,
881
+ link: navLink,
882
+ count: navCount,
883
+ };
884
+ sectionViews.push(sectionView);
885
+ navLink.addEventListener('focus', function () {
886
+ var task = section.kind === 'checklist' ? preferredTaskInSection(sectionIndex) : null;
887
+ setKeyboardCursor(sectionIndex, task && task.id);
888
+ });
889
+ navLink.addEventListener('click', function () {
890
+ var task = section.kind === 'checklist' ? preferredTaskInSection(sectionIndex) : null;
891
+ setKeyboardCursor(sectionIndex, task && task.id);
892
+ });
209
893
  });
210
- updateProgress();
894
+ shell.append(navigation, checklistBody);
895
+ content.append(shell);
896
+ updateDashboard();
897
+ var initialTask = firstUnresolvedTask();
898
+ setKeyboardCursor(initialTask ? initialTask.sectionIndex : 0, initialTask && initialTask.id, { focus: true, scroll: true });
211
899
  setStatus('Opened ' + entry.identity + '.');
212
900
  }
213
901
 
214
902
  function showMessage(message, isError) {
215
903
  currentEntry = null;
216
- currentStates = {};
904
+ currentStates = Object.create(null);
905
+ taskViews = Object.create(null);
906
+ taskSequence = [];
907
+ groupViews = [];
908
+ sectionViews = [];
909
+ keyboardCursor = null;
217
910
  document.title = 'Checklist library';
218
911
  document.getElementById('title').textContent = 'Choose a checklist';
219
912
  document.getElementById('summary').textContent = 'Browse the checklists generated from this repository.';
220
913
  content.className = isError ? 'error' : 'empty';
221
914
  content.textContent = message;
222
- updateProgress();
915
+ updateDashboard();
223
916
  }
224
917
 
225
918
  function renderLibrary(selectedIdentity) {
226
919
  document.documentElement.dataset.viewerReady = 'false';
227
- picker.replaceChildren(element('option', '', payload && payload.checklists.length ? 'Choose a checklist...' : 'No checklists found'));
920
+ var hasChecklists = payload && Array.isArray(payload.checklists) && payload.checklists.length;
921
+ picker.replaceChildren(element('option', '', hasChecklists ? 'Choose a checklist...' : 'No checklists found'));
228
922
  picker.firstChild.value = '';
229
923
  if (!payload || !Array.isArray(payload.checklists)) {
230
924
  showMessage('No generated payload was loaded. Run the checklist build script, then reload this page.', true);
@@ -232,7 +926,7 @@
232
926
  return;
233
927
  }
234
928
 
235
- payload.categories.forEach(function (category) {
929
+ (payload.categories || []).forEach(function (category) {
236
930
  var group = element('optgroup');
237
931
  group.label = category;
238
932
  payload.checklists.filter(function (entry) { return entry.category === category; }).forEach(function (entry) {
@@ -256,14 +950,15 @@
256
950
  }
257
951
 
258
952
  function exportProgress() {
259
- if (!currentEntry) return setStatus('Choose a checklist before exporting progress.');
953
+ if (!currentEntry) return setActionStatus('Choose a checklist before exporting progress.', true);
260
954
  var blob = new Blob([JSON.stringify(progressRecord(), null, 2) + '\n'], { type: 'application/json' });
261
955
  var anchor = document.createElement('a');
262
956
  anchor.href = URL.createObjectURL(blob);
263
957
  anchor.download = currentEntry.identity.replaceAll('/', '-') + '.progress.json';
264
958
  anchor.click();
265
959
  URL.revokeObjectURL(anchor.href);
266
- setStatus('Progress export created.');
960
+ setActionStatus('Progress export created.');
961
+ actionMenu.open = false;
267
962
  }
268
963
 
269
964
  function importProgress(file) {
@@ -279,12 +974,13 @@
279
974
  currentStates = normalizedStates(currentEntry, imported.states);
280
975
  saveProgress();
281
976
  renderChecklist(currentEntry, currentStates);
282
- setStatus('Progress imported.');
977
+ setActionStatus('Progress imported.');
978
+ actionMenu.open = false;
283
979
  } catch (error) {
284
- setStatus('Progress import failed: ' + error.message);
980
+ setActionStatus('Progress import failed: ' + error.message, true);
285
981
  }
286
982
  });
287
- reader.addEventListener('error', function () { setStatus('Progress import failed: the file could not be read.'); });
983
+ reader.addEventListener('error', function () { setActionStatus('Progress import failed: the file could not be read.', true); });
288
984
  reader.readAsText(file);
289
985
  }
290
986
 
@@ -308,7 +1004,7 @@
308
1004
  script.addEventListener('error', function () {
309
1005
  refreshPending = false;
310
1006
  script.remove();
311
- setStatus('Could not refresh the generated checklist payload.');
1007
+ setStatus('Could not refresh the generated checklist payload.', true);
312
1008
  });
313
1009
  document.head.append(script);
314
1010
  }
@@ -320,19 +1016,23 @@
320
1016
  });
321
1017
  document.getElementById('export-progress').addEventListener('click', exportProgress);
322
1018
  document.getElementById('import-progress').addEventListener('click', function () {
323
- if (!currentEntry) return setStatus('Choose a checklist before importing progress.');
1019
+ if (!currentEntry) return setActionStatus('Choose a checklist before importing progress.', true);
324
1020
  document.getElementById('import-file').click();
325
1021
  });
326
1022
  document.getElementById('import-file').addEventListener('change', function (event) {
327
1023
  importProgress(event.target.files[0]);
328
1024
  event.target.value = '';
329
1025
  });
330
- document.getElementById('reset-progress').addEventListener('click', function () {
331
- if (!currentEntry || !window.confirm('Clear progress for this checklist?')) return;
332
- currentStates = {};
333
- saveProgress();
334
- renderChecklist(currentEntry);
335
- setStatus('Progress reset.');
1026
+ document.getElementById('reset-progress').addEventListener('click', resetProgress);
1027
+ document.getElementById('current-task-link').addEventListener('click', function (event) {
1028
+ var task = currentEntry && firstUnresolvedTask();
1029
+ if (!task) return;
1030
+ event.preventDefault();
1031
+ setKeyboardCursor(task.sectionIndex, task.id, { focus: true, scroll: true });
1032
+ });
1033
+ document.addEventListener('keydown', handleKeyboardShortcut);
1034
+ document.addEventListener('click', function (event) {
1035
+ if (actionMenu.open && !actionMenu.contains(event.target)) actionMenu.open = false;
336
1036
  });
337
1037
  window.addEventListener('focus', refreshPayload);
338
1038