@kaydash9999/banker-plugins 0.22.0 → 0.23.2

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.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +68 -0
  4. package/PRIVACY.md +1 -0
  5. package/README.md +39 -13
  6. package/bin/banker.js +118 -22
  7. package/bin/lib/codex-hooks.mjs +116 -18
  8. package/bin/lib/codex-names.mjs +4 -0
  9. package/bin/lib/codex-routes.mjs +93 -1
  10. package/bin/lib/command-skill.cjs +37 -6
  11. package/codex/command-skills/front-qa/SKILL.md +1 -1
  12. package/codex/command-skills/setup/SKILL.md +4 -4
  13. package/codex/hooks.json +5 -5
  14. package/codex/manifest.json +18 -15
  15. package/codex/skills/progress/scripts/progress.mjs +44 -14
  16. package/codex/skills/teamwork/SKILL.md +143 -0
  17. package/codex/skills/teamwork/scripts/teamwork.mjs +1007 -0
  18. package/codex/transform-matrix.md +5 -3
  19. package/hooks/obsidize-hook.mjs +75 -32
  20. package/hooks/telemetry-count-codex.mjs +238 -57
  21. package/hooks/update-fetch.mjs +2 -1
  22. package/hooks/update-notify.mjs +82 -20
  23. package/package.json +3 -1
  24. package/skills/3d-intro-build/SKILL.md +101 -20
  25. package/skills/3d-intro-build/references/NOTICE.md +9 -2
  26. package/skills/3d-intro-build/references/assemble.mjs +113 -22
  27. package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
  28. package/skills/3d-intro-build/references/clip-post.mjs +443 -0
  29. package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
  30. package/skills/3d-intro-build/references/intro-fixes.css +11 -0
  31. package/skills/3d-intro-build/references/prompts.md +51 -4
  32. package/skills/3d-intro-build/references/scrub-engine.js +1 -0
  33. package/skills/3d-intro-build/references/step-nav.css +29 -0
  34. package/skills/3d-intro-build/references/step-nav.js +284 -106
  35. package/skills/3d-intro-build/references/verify-intro.mjs +373 -32
  36. package/skills/3d-intro-build/references/verify.md +6 -0
  37. package/skills/3d-intro-build/references/video-pool.mjs +142 -12
  38. package/skills/3d-intro-setup/SKILL.md +20 -7
  39. package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
  40. package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
  41. package/skills/all-in-one/SKILL.md +125 -86
  42. package/skills/omc-patch/SKILL.md +1 -1
  43. package/skills/payload-mon/SKILL.md +1 -1
  44. package/skills/play-qa/SKILL.md +6 -4
  45. package/skills/progress-mon/README.md +164 -0
  46. package/skills/progress-mon/SKILL.md +165 -0
  47. package/skills/progress-mon/assets/fixture/README.md +1 -0
  48. package/skills/progress-mon/assets/fixture/arch.json +33 -0
  49. package/skills/progress-mon/assets/fixture/commits.json +7 -0
  50. package/skills/progress-mon/assets/fixture/config.json +30 -0
  51. package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
  52. package/skills/progress-mon/assets/fixture/flow.json +44 -0
  53. package/skills/progress-mon/assets/fixture/items.json +14 -0
  54. package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
  55. package/skills/progress-mon/assets/fixture/reqs.json +11 -0
  56. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
  57. package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
  58. package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
  59. package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
  60. package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
  61. package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
  62. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
  63. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
  64. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
  65. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
  66. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
  67. package/skills/progress-mon/references/arch.md +280 -0
  68. package/skills/progress-mon/references/authoring.md +199 -0
  69. package/skills/progress-mon/references/config.md +115 -0
  70. package/skills/progress-mon/references/flow.md +75 -0
  71. package/skills/progress-mon/references/inventory.md +58 -0
  72. package/skills/progress-mon/references/items.md +74 -0
  73. package/skills/progress-mon/references/req-spec.md +74 -0
  74. package/skills/progress-mon/references/reqs.md +30 -0
  75. package/skills/progress-mon/scripts/index.js +31 -0
  76. package/skills/progress-mon/scripts/pm-check.mjs +549 -0
  77. package/skills/progress-mon/scripts/pm-config.mjs +343 -0
  78. package/skills/progress-mon/scripts/pm-data.mjs +394 -0
  79. package/skills/progress-mon/scripts/pm-html.mjs +77 -0
  80. package/skills/progress-mon/scripts/pm-memory.mjs +333 -0
  81. package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
  82. package/skills/progress-mon/scripts/pm-server.mjs +781 -0
  83. package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
  84. package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
  85. package/skills/ralph-qa/SKILL.md +107 -17
  86. package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
  87. package/skills/ralph-qa/references/payload-scan.mjs +19 -2
  88. package/skills/ralph-qa/references/run-folder.mjs +299 -0
  89. package/skills/ralph-qa/references/seat-payload.mjs +60 -13
  90. package/skills/ralph-qa/references/seat-run.mjs +200 -159
  91. package/skills/remains/SKILL.md +2 -0
  92. package/skills/remains/scripts/boxes.mjs +6 -2
  93. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
  94. package/skills/setup-mcp/SKILL.md +1 -1
  95. package/skills/setup-omc-hud/SKILL.md +2 -1
  96. package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
  97. package/skills/ultra-chain/SKILL.md +32 -14
  98. package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
  99. package/skills/ultra-init/SKILL.md +30 -22
  100. package/skills/ultraqa/NOTICE.md +34 -0
  101. package/skills/ultraqa/SKILL.md +197 -0
  102. package/skills/ultraqa/references/gate-evidence.mjs +457 -0
  103. package/skills/ultraqa/references/run-folder.mjs +299 -0
  104. package/skills/ultraqa/references/tmux-drive.md +52 -0
  105. package/skills/ultraqa/references/uqa-run.mjs +339 -0
  106. package/skills/update-banker/SKILL.md +1 -1
@@ -1,13 +1,23 @@
1
1
  /*
2
- * Scene-by-scene navigation for the scroll-world intro (opt-in: intro.json "stepNav").
2
+ * Scene-by-scene navigation for the scroll-world intro (on by default; intro.json "stepNav": false
3
+ * turns it off).
3
4
  *
4
5
  * The vendored engine maps the window's scroll position to clip time and stays as shipped; this
5
- * script only moves the scroll position. One wheel gesture, arrow key, PageDown/PageUp, Space or a
6
- * swipe of more than 30 px moves exactly one scene; a route dot goes to its scene. Inputs during a
7
- * move keep the latest one, run when the move ends. Stops: the first scene at 0, middle scenes in
8
- * the middle of their hold, the last scene at the end of its clip, then the top of the page body
9
- * (`end`, when the intro sits on top of a site). Below the body top the browser scrolls as usual;
10
- * one up input at the body top returns to the last scene. A move starts from where the page stands.
6
+ * script only moves the scroll position. Stops: each scene's start (its dive's first frame; an exit
7
+ * clip is the way to the next scene), the end of the last scene, then the top of the page body
8
+ * (`end`, when the intro sits on top of a site). Between two stops lies a half point.
9
+ * wheel one notch moves half way to the next stop, two notches a whole scene. Pixel, line
10
+ * and page deltas, and a touchpad's small ones, add up to notches (notchPx, linePx);
11
+ * one gesture moves at most maxNotches
12
+ * arrow keys up/down and left/right as the wheel (up and left go back)
13
+ * PageUp/PageDown, Space (shift goes back), a swipe over swipePx one scene
14
+ * Home, End the first scene's start, the last scene's end, at once (no animation)
15
+ * a route dot its scene's start, at once (a click, or Enter or Space on the focused dot)
16
+ * Fields and editable places keep their keys. An input during a move retargets it from where that
17
+ * move was going. When the page comes to rest anywhere else above the body (a scrollbar, a touchpad,
18
+ * an anchor), it settles on the nearest stop after snapMs. Below the body top the browser scrolls as
19
+ * usual. At a stop or a half point, the copy of the scene in view is shown (class isn-copy, faded by
20
+ * step-nav.css; the engine's own fade takes over again when a move starts).
11
21
  *
12
22
  * Options (intro.json "stepNav", all optional; values are the defaults below):
13
23
  * end selector of the body start; omit on a standalone intro page. It is looked up at
@@ -18,8 +28,14 @@
18
28
  * swipePx a touch swipe longer than this is one step
19
29
  * durBaseMs, durPerVhMs, durMaxMs a move takes min(durMax, durBase + distance in screen heights
20
30
  * x durPerVh) ms with a cosine ease; 0 under prefers-reduced-motion
21
- * lastGuardMs PROVISIONAL (still being verified where it came from): a down input queued on the
22
- * way into the last scene is dropped, and down is ignored this long after arriving
31
+ * notchPx, linePx, maxNotches wheel pixels to a notch, pixels in a line delta, notches per gesture
32
+ * snapMs how long the page must rest off the stops before it settles on the nearest one
33
+ * decodeCapMs, instantDecodeCapMs the longest wait for the posters to decode before a move, and before
34
+ * an instant one (nothing hides an undecoded poster there, so it waits longer)
35
+ * loadingDelayMs, loadingLabel a wait for the posters (either kind of move) longer than loadingDelayMs
36
+ * shows a small loading mark mid-screen with this text (a status region, read by a screen
37
+ * reader; it takes no focus and no input, and a new input drops it with the old move). It
38
+ * goes as the move starts. Under reduced motion the mark does not spin
23
39
  * prefetch download the clips at load so the first move already plays video; on a phone or
24
40
  * with Save-Data only the first three in playback order
25
41
  * hintLabel, skipLabel, mainLabel texts of the mouse hint, the skip link and the main button
@@ -37,7 +53,8 @@
37
53
 
38
54
  const DEFAULTS = Object.freeze({
39
55
  end: null, gestureGapMs: 220, minWheel: 2, swipePx: 30,
40
- durBaseMs: 2600, durPerVhMs: 760, durMaxMs: 5600, lastGuardMs: 250, prefetch: true,
56
+ durBaseMs: 2600, durPerVhMs: 760, durMaxMs: 5600, decodeCapMs: 300, prefetch: true,
57
+ notchPx: 100, linePx: 40, maxNotches: 2, snapMs: 400, instantDecodeCapMs: 1500, loadingDelayMs: 150, loadingLabel: 'Loading',
41
58
  hintLabel: 'Scroll or press the down key', skipLabel: 'Skip intro', mainLabel: 'Go to main content',
42
59
  });
43
60
 
@@ -63,16 +80,64 @@
63
80
  /** The scroll length of the segments in screen heights (the engine's track adds one screen). */
64
81
  function totalWeight(config) { return spansOf(config).total; }
65
82
 
66
- /** Stop positions in px: the first scene at 0, middle scenes mid-dive, the last at its end, then the body. */
83
+ /** Stop positions in px: each scene's start, the last scene's end, then the body. */
67
84
  function stopPositions(config, vh, endTop) {
68
85
  const spans = spansOf(config).spans;
69
- const stops = spans.map(function (span, i) {
70
- if (i === 0) return 0;
71
- return Math.round((i === spans.length - 1 ? span[1] : (span[0] + span[1]) / 2) * vh);
72
- });
86
+ // one pixel into the scene: the engine's current segment is the last whose start is at or above the
87
+ // page, and its starts carry float error (2.2 x 800 = 1760.0000000000002), so a stop at the rounded
88
+ // start would leave the segment before on top; an exit ending on a background plate showed the plate
89
+ const stops = spans.map(function (span, i) { return i ? Math.floor(span[0] * vh) + 1 : 0; });
90
+ if (spans.length) stops.push(Math.round(spans[spans.length - 1][1] * vh));
73
91
  return endTop == null ? stops : stops.concat([endTop]);
74
92
  }
75
93
 
94
+ /** The stops with the half point between each two: where a wheel notch or an arrow key can land. */
95
+ function gridOf(stops) {
96
+ const g = [];
97
+ stops.forEach(function (v, i) { if (i) g.push(Math.round((stops[i - 1] + v) / 2)); g.push(v); });
98
+ return g;
99
+ }
100
+
101
+ /**
102
+ * Where n points of `list` (signed: + goes down) take the page from `base`; from a place between
103
+ * points the first one is the next point that way. null when there is none that way.
104
+ */
105
+ function stepFrom(list, base, n) {
106
+ if (!n) return null;
107
+ let i;
108
+ if (n > 0) i = list.findIndex(function (v) { return v > base + 1; });
109
+ else for (i = list.length - 1; i >= 0 && !(list[i] < base - 1); i--);
110
+ if (i < 0) return null;
111
+ return list[Math.max(0, Math.min(list.length - 1, i + (n > 0 ? n - 1 : n + 1)))];
112
+ }
113
+
114
+ /** The scene whose copy is in view at y: the dive holding it, or across an exit the nearer scene. */
115
+ function sceneAt(config, vh, y) {
116
+ const spans = spansOf(config).spans;
117
+ for (let i = spans.length - 1; i >= 0; i--) {
118
+ if (y >= spans[i][0] * vh - 1) {
119
+ const end = spans[i][1] * vh;
120
+ const next = spans[i + 1];
121
+ return y <= end + 1 || !next ? i : (y - end > (next[0] * vh - end) / 2 ? i + 1 : i);
122
+ }
123
+ }
124
+ return 0;
125
+ }
126
+
127
+ /** The stop a page resting at y settles on (the nearest up to the last scene's end); -1 at a stop or a half point. */
128
+ function settleTarget(stops, lastStop, y) {
129
+ if (gridOf(stops).some(function (v) { return Math.abs(v - y) <= 1; })) return -1;
130
+ return nearest(stops.slice(0, lastStop + 1), y);
131
+ }
132
+
133
+ /** Wheel notches in one event: pixel, line and page deltas in pixels, added to the gesture's remainder. */
134
+ function wheelNotches(e, carry, o) {
135
+ const unit = e.deltaMode === 1 ? o.linePx : e.deltaMode === 2 ? 2 * o.notchPx : 1;
136
+ const total = carry + (e.deltaY || 0) * unit;
137
+ const n = Math.trunc(total / o.notchPx);
138
+ return { n: n, carry: total - n * o.notchPx };
139
+ }
140
+
76
141
  function moveDuration(distance, vh, o, reduce) {
77
142
  if (reduce) return 0;
78
143
  return Math.min(o.durMaxMs, o.durBaseMs + (Math.abs(distance) / vh) * o.durPerVhMs);
@@ -133,67 +198,49 @@
133
198
  return best;
134
199
  };
135
200
 
136
- // The stop an input asks for from position `pos`: a dot's scene, the next stop below for down, the
137
- // next above for up; -1 when there is none.
138
- function targetOf(stops, pos, input) {
139
- if (typeof input === 'object') return Math.max(0, Math.min(stops.length - 1, input.to));
140
- if (input === 'down') return stops.findIndex(function (v) { return v > pos + 1; });
141
- for (let i = stops.length - 1; i >= 0; i--) if (stops[i] < pos - 1) return i;
142
- return -1;
201
+ // Where an input asks to go from `base`: { to: i } a stop, { steps: n } n grid points, { scenes: n } the
202
+ // next stop that way (n is +1 or -1). null when there is nothing that way.
203
+ function targetOf(stops, base, input) {
204
+ if (input.to != null) return stops[Math.max(0, Math.min(stops.length - 1, input.to))];
205
+ if (input.steps) return stepFrom(gridOf(stops), base, input.steps);
206
+ return stepFrom(stops, base, input.scenes);
143
207
  }
144
208
 
145
209
  /**
146
- * Moves between stops. input('down' | 'up' | { to }) while idle starts a move from where the page
147
- * stands (sync(y)); during a move it replaces the queued input. move(fromY, toY, done) animates;
148
- * onArrive(index) runs after each move.
210
+ * Moves between stops and half points. input(spec) starts a move from where the page stands (sync(y)),
211
+ * or during a move retargets it from where it was going. move(fromY, toY, done) animates from the
212
+ * page's place (where()); onArrive(y) runs when a move ends; onLeave() when one starts.
149
213
  */
150
214
  function createNavigator(opt) {
151
- let index = 0;
152
215
  let pos = 0;
153
- let target = -1;
154
- let queued = null;
155
- let guardUntil = -Infinity;
216
+ let target = null;
156
217
  let generation = 0;
218
+ let arrivals = 0;
157
219
  const list = function () { return opt.stops(); };
158
- function arrive(from, to) {
159
- index = to;
160
- pos = list()[to];
161
- target = -1;
162
- if (to === opt.lastScene && from < to) guardUntil = opt.now() + opt.guardMs;
163
- if (opt.onArrive) opt.onArrive(to);
164
- const next = queued;
165
- queued = null;
166
- if (next) go(next);
167
- }
168
- function go(input) {
169
- const to = targetOf(list(), pos, input);
170
- if (to < 0 || Math.abs(list()[to] - pos) <= 1) return;
171
- if (input === 'down' && index === opt.lastScene && opt.now() < guardUntil) return;
172
- const from = index;
173
- const gen = generation;
174
- target = to;
175
- opt.move(pos, list()[to], function () { if (gen === generation) arrive(from, to); });
220
+ function go(y, instant) {
221
+ if (y == null || Math.abs(y - (target == null ? pos : target)) <= 1) return;
222
+ const gen = ++generation;
223
+ if (target == null && opt.onLeave) opt.onLeave(instant);
224
+ target = y;
225
+ opt.move(opt.where ? opt.where() : pos, y, function () {
226
+ if (gen !== generation) return;
227
+ pos = y;
228
+ target = null;
229
+ arrivals++;
230
+ if (opt.onArrive) opt.onArrive(y);
231
+ }, Boolean(instant));
176
232
  }
177
233
  return {
178
- input: function (input) {
179
- if (target === -1) return go(input);
180
- if (input === 'down' && target === opt.lastScene && target > index) return;
181
- queued = input;
182
- },
183
- sync: function (y) {
184
- if (target !== -1) return;
185
- pos = y;
186
- index = nearest(list(), y);
187
- },
188
- // Drops the move in flight and the queued input; the move's end, when it comes, starts nothing.
189
- cancel: function () {
190
- generation++;
191
- target = -1;
192
- queued = null;
193
- },
234
+ input: function (spec) { go(targetOf(list(), target == null ? pos : target, spec), spec.instant); },
235
+ sync: function (y) { if (target == null) pos = y; },
236
+ // Drops the move in flight; its end, when it comes, does nothing.
237
+ cancel: function () { generation++; target = null; },
194
238
  state: function () {
195
- const n = list().length;
196
- return { index: index, moving: target !== -1, queued: queued, atBody: n - 1 > opt.lastScene && index === n - 1 };
239
+ const stops = list();
240
+ const at = target == null ? pos : target;
241
+ const index = nearest(stops, at);
242
+ return { index: index, y: at, moving: target != null, arrivals: arrivals, onStop: Math.abs(stops[index] - at) <= 1,
243
+ atBody: stops.length - 1 > opt.lastStop && index === stops.length - 1 && Math.abs(stops[index] - at) <= 1 };
197
244
  },
198
245
  };
199
246
  }
@@ -207,15 +254,26 @@
207
254
  const pressable = function (t) {
208
255
  return Boolean(t) && (/^(BUTTON|A|SUMMARY)$/.test(t.tagName || '') || /^(button|link|checkbox|switch|menuitem|tab|option)$/.test(role(t)));
209
256
  };
210
- const KEYS = { ArrowDown: 'down', PageDown: 'down', ArrowUp: 'up', PageUp: 'up' };
257
+ // The intro's own controls (the route dots and the nav items) keep moving the scene on Left/Right after a click
258
+ // focuses them; another focused button or link (in scene content) leaves Left/Right to itself.
259
+ const ownControl = function (t) { return Boolean(t && t.closest && t.closest('.sw-nav, .sw-route')); };
260
+ const KEYS = { ArrowDown: { steps: 1 }, ArrowRight: { steps: 1 }, ArrowUp: { steps: -1 }, ArrowLeft: { steps: -1 },
261
+ PageDown: { scenes: 1 }, PageUp: { scenes: -1 }, Home: { to: 0, instant: true }, End: { to: 'last', instant: true } };
211
262
 
212
- /** The step a key asks for, or null when the key is not the intro's. */
263
+ /** What a key asks for ({ steps }, { scenes }, { to }; End's to is 'last'), or null when the key is not the intro's. */
213
264
  function keyInput(e) {
214
265
  if (e.altKey || e.ctrlKey || e.metaKey || editable(e.target)) return null;
215
- if (e.key === ' ') return pressable(e.target) ? null : (e.shiftKey ? 'up' : 'down');
266
+ if (e.key === ' ') return pressable(e.target) ? null : { scenes: e.shiftKey ? -1 : 1 };
267
+ if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && pressable(e.target) && !ownControl(e.target)) return null;
216
268
  return KEYS[e.key] || null;
217
269
  }
218
270
 
271
+ /** The direction of an input from `base`: 'down', 'up', or null when it asks for no move. */
272
+ function dirOf(stops, base, input) {
273
+ const y = targetOf(stops, base, input);
274
+ return y == null || Math.abs(y - base) <= 1 ? null : (y > base ? 'down' : 'up');
275
+ }
276
+
219
277
  // ---- browser wiring -----------------------------------------------------------
220
278
 
221
279
  const ease = function (p) { return 0.5 - 0.5 * Math.cos(Math.PI * p); };
@@ -226,9 +284,46 @@
226
284
  try { window.scrollTo({ top: y, behavior: 'instant' }); } catch (e) { window.scrollTo(0, y); }
227
285
  }
228
286
 
287
+ // A scene a move shows paints nothing until its poster is decoded (the engine's posters decode async):
288
+ // a jump showed an empty background for 0.1-0.35 s. Decode every poster first, waiting at most capMs.
289
+ function postersReady(world, capMs, then) {
290
+ let fired = false;
291
+ const go = function () { if (!fired) { fired = true; then(); } };
292
+ const stills = Array.prototype.slice.call(world.querySelectorAll('.sw-scene__still'));
293
+ Promise.all(stills.map(function (i) { return i.decode ? i.decode().catch(function () {}) : null; })).then(go);
294
+ setTimeout(go, capMs);
295
+ }
296
+
297
+ /**
298
+ * The loading mark of the poster waits: begin(token) shows it after delayMs unless end(token) came first;
299
+ * a newer begin() or cancel() drops the older wait and its mark. show() and hide() draw it; timers injectable.
300
+ */
301
+ function createLoader(delayMs, show, hide, timers) {
302
+ // wrapped: a browser refuses setTimeout called as a method of another object (Illegal invocation)
303
+ const t = timers || { set: function (f, ms) { return setTimeout(f, ms); }, clear: function (id) { clearTimeout(id); } };
304
+ let current = null;
305
+ let timer = null;
306
+ let shown = false;
307
+ function drop() {
308
+ if (timer !== null) t.clear(timer);
309
+ timer = null;
310
+ if (shown) { shown = false; hide(); }
311
+ }
312
+ return {
313
+ begin: function (token) {
314
+ drop();
315
+ current = token;
316
+ timer = t.set(function () { timer = null; if (current === token) { shown = true; show(); } }, delayMs);
317
+ },
318
+ end: function (token) { if (token === current) { drop(); current = null; } },
319
+ cancel: function () { drop(); current = null; },
320
+ };
321
+ }
322
+
229
323
  // The running animation; a newer one, or stopAnimation(), ends it between frames.
230
324
  let animation = 0;
231
- function stopAnimation() { animation++; }
325
+ let pending = 0; // a move waiting for its posters; a newer move or stopAnimation() drops it
326
+ function stopAnimation() { animation++; pending++; }
232
327
 
233
328
  function animate(from, to, duration, done) {
234
329
  const mine = ++animation;
@@ -321,23 +416,39 @@
321
416
  document.body.insertBefore(skip, document.body.firstChild);
322
417
  document.body.appendChild(hint);
323
418
  if (o.end) document.body.appendChild(main);
324
- return { hint: hint, skip: skip, main: main };
419
+ // a status region: its text is set while a poster wait shows, so a screen reader hears it; no focus taken
420
+ const loading = element('div', 'isn-loading');
421
+ loading.setAttribute('role', 'status');
422
+ loading.setAttribute('aria-live', 'polite');
423
+ const spin = element('i', 'isn-loading__spin');
424
+ spin.setAttribute('aria-hidden', 'true');
425
+ loading.appendChild(spin);
426
+ loading.appendChild(element('span', 'isn-loading__label'));
427
+ document.body.appendChild(loading);
428
+ return { hint: hint, skip: skip, main: main, loading: loading };
325
429
  }
326
430
 
327
- /** Wires the navigation to the page. Returns { goTo(i), state(), stops(), lastScene, end }. */
431
+ /**
432
+ * Wires the navigation to the page. Returns { goTo(i), hold(on), state(), stops(), lastScene, lastStop, end };
433
+ * hold(true) stops the settling on a stop (a measuring script that parks the page between stops).
434
+ */
328
435
  function attach(world, config, options) {
329
436
  const o = Object.assign({}, DEFAULTS, options || {});
330
437
  const ctx = makeContext(world, config, o);
331
438
  ctx.ui = buildUi(o);
439
+ const label = ctx.ui.loading.querySelector('.isn-loading__label');
440
+ ctx.loader = createLoader(o.loadingDelayMs,
441
+ function () { label.textContent = o.loadingLabel; ctx.ui.loading.classList.add('is-on'); },
442
+ function () { ctx.ui.loading.classList.remove('is-on'); label.textContent = ''; });
332
443
  listenWheel(ctx);
333
444
  listenKeys(ctx);
334
445
  listenTouch(ctx);
335
446
  listenClicks(ctx);
336
447
  listenScroll(ctx);
337
448
  ctx.nav.sync(window.scrollY);
338
- ctx.mark(ctx.nav.state().index);
339
- const handle = { goTo: function (i) { ctx.nav.sync(window.scrollY); ctx.nav.input({ to: i }); },
340
- state: function () { return ctx.nav.state(); }, stops: ctx.stops, lastScene: ctx.lastScene, end: o.end };
449
+ ctx.mark();
450
+ const handle = { goTo: function (i) { step(ctx, { to: i }); }, hold: function (on) { ctx.held = Boolean(on); },
451
+ state: function () { return ctx.nav.state(); }, stops: ctx.stops, lastScene: ctx.lastScene, lastStop: ctx.lastStop, end: o.end };
341
452
  // The navigation on this page, for scripts and for verify-intro.mjs.
342
453
  window.IntroStepNav.current = handle;
343
454
  return handle;
@@ -346,7 +457,7 @@
346
457
  function makeContext(world, config, o) {
347
458
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
348
459
  const lastScene = (config.sections || []).length - 1;
349
- const ctx = { o: o, world: world, lastScene: lastScene, focusEnd: false, warned: false };
460
+ const ctx = { o: o, world: world, config: config, lastScene: lastScene, lastStop: lastScene + 1, focusEnd: false, warned: false };
350
461
  ctx.endEl = function () { return endElement(ctx); };
351
462
  ctx.endTop = function () { const e = ctx.endEl(); return e ? Math.round(e.getBoundingClientRect().top + window.scrollY) : null; };
352
463
  // The engine sizes its track (segments + one screen) from the height it last laid out at, so the
@@ -357,11 +468,24 @@
357
468
  return h > 0 ? h / (totalWeight(config) + 1) : window.innerHeight;
358
469
  };
359
470
  ctx.stops = function () { return stopPositions(config, ctx.vh(), ctx.endTop()); };
360
- ctx.ours = function (y, dir) { return belongsToIntro(y, dir, ctx.stops()[lastScene], ctx.endTop()); };
361
- ctx.mark = function (i) { mark(ctx, i); };
362
- ctx.nav = createNavigator({ stops: ctx.stops, lastScene: lastScene, guardMs: o.lastGuardMs,
363
- now: function () { return performance.now(); }, onArrive: ctx.mark,
364
- move: function (from, to, done) { animate(from, to, moveDuration(to - from, ctx.vh(), o, reduce), done); } });
471
+ ctx.ours = function (y, dir) { return belongsToIntro(y, dir, ctx.stops()[ctx.lastStop], ctx.endTop()); };
472
+ ctx.mark = function () { mark(ctx); };
473
+ ctx.nav = createNavigator({ stops: ctx.stops, lastStop: ctx.lastStop, where: function () { return window.scrollY; },
474
+ onLeave: function (instant) { instantCopy(instant); showCopy(ctx, -1); },
475
+ onArrive: function () { ctx.mark(); requestAnimationFrame(function () { requestAnimationFrame(function () { instantCopy(false); }); }); },
476
+ move: function (from, to, done, instant) {
477
+ const mine = ++pending;
478
+ // an instant move swaps the copy at once even when it retargets a move already under way (no onLeave then)
479
+ instantCopy(instant);
480
+ ctx.loader.begin(mine);
481
+ // from where the page is once the posters are ready: an earlier move may have run on while waiting
482
+ postersReady(world, instant ? o.instantDecodeCapMs : o.decodeCapMs, function () {
483
+ if (mine !== pending) return;
484
+ ctx.loader.end(mine);
485
+ const at = window.scrollY;
486
+ animate(at, to, instant ? 0 : moveDuration(to - at, ctx.vh(), o, reduce), done);
487
+ });
488
+ } });
365
489
  const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
366
490
  document.documentElement.classList.add('isn-on', fine ? 'isn-mouse' : 'isn-touch');
367
491
  return ctx;
@@ -387,44 +511,75 @@
387
511
  e.focus({ preventScroll: true });
388
512
  }
389
513
 
390
- // Classes the CSS reads: isn-last at the last scene; isn-ready once its clip has played to the end
391
- // (the page stands at the last stop; under reduced motion that stop is reached at once and the
392
- // engine plays no clip); isn-body at or below the body top. Moves the focus to the body after the
393
- // main button.
394
- function mark(ctx, i) {
514
+ // An instant move (Home, End, a dot) swaps the copy without the fade (class isn-instant, step-nav.css).
515
+ function instantCopy(on) { document.documentElement.classList.toggle('isn-instant', Boolean(on)); }
516
+
517
+ // The copy of scene k shows over the engine's own fade (class isn-copy, see step-nav.css); -1 shows none.
518
+ // The same element: nothing is copied, so a screen reader and the focus meet each text once.
519
+ function showCopy(ctx, k) {
520
+ ctx.world.querySelectorAll('.sw-copy').forEach(function (c, i) { c.classList.toggle('isn-copy', i === k); });
521
+ }
522
+
523
+ // Classes the CSS reads: isn-last at the last scene's end; isn-ready there once its clip has played
524
+ // to the end (under reduced motion that stop is reached at once and the engine plays no clip);
525
+ // isn-body at or below the body top. At a stop or a half point the copy in view shows. Moves the
526
+ // focus to the body after the main button.
527
+ function mark(ctx) {
395
528
  const c = document.documentElement.classList;
396
- const atEnd = Math.abs(window.scrollY - ctx.stops()[ctx.lastScene]) <= 1;
397
- c.toggle('isn-last', i === ctx.lastScene);
398
- c.toggle('isn-ready', Boolean(ctx.endEl()) && i === ctx.lastScene && atEnd);
399
- c.toggle('isn-body', i > ctx.lastScene);
400
- if (ctx.focusEnd && i > ctx.lastScene) focusBody(ctx);
529
+ const y = window.scrollY;
530
+ const stops = ctx.stops();
531
+ const atLast = Math.abs(y - stops[ctx.lastStop]) <= 1;
532
+ const body = stops.length - 1 > ctx.lastStop && y >= stops[stops.length - 1] - 1;
533
+ c.toggle('isn-last', atLast);
534
+ c.toggle('isn-ready', Boolean(ctx.endEl()) && atLast);
535
+ c.toggle('isn-body', body);
536
+ const onGrid = gridOf(stops).some(function (v) { return Math.abs(v - y) <= 1; });
537
+ showCopy(ctx, onGrid && !body ? sceneAt(ctx.config, ctx.vh(), y) : -1);
538
+ if (ctx.focusEnd && body) focusBody(ctx);
401
539
  }
402
540
 
403
- function step(ctx, dir) {
541
+ // An input ({ steps }, { scenes }, { to }, End's { to: 'last' }; instant: no animation) from where the page stands.
542
+ function step(ctx, input) {
404
543
  ctx.nav.sync(window.scrollY);
405
- ctx.nav.input(dir);
544
+ ctx.nav.input(input.to === 'last' ? { to: ctx.lastStop, instant: input.instant } : input);
406
545
  }
407
546
 
547
+ // The direction an input takes from where the page (or the move in flight) is going.
548
+ const dirFrom = function (ctx, input) {
549
+ const st = ctx.nav.state();
550
+ return dirOf(ctx.stops(), st.moving ? st.y : window.scrollY, input.to === 'last' ? { to: ctx.lastStop, instant: input.instant } : input);
551
+ };
552
+
408
553
  function listenWheel(ctx) {
409
554
  const gesture = createWheelGesture(ctx.o);
555
+ let carry = 0;
556
+ let used = 0;
410
557
  window.addEventListener('wheel', function (e) {
411
558
  if (wheelIgnored(e)) return;
412
- const dir = e.deltaY > 0 ? 'down' : 'up';
413
- if (!ctx.ours(window.scrollY, dir)) return;
559
+ if (!ctx.ours(window.scrollY, e.deltaY > 0 ? 'down' : 'up')) return;
414
560
  e.preventDefault();
415
- if (gesture.feed(e, e.timeStamp) === 'new') step(ctx, dir);
561
+ const kind = gesture.feed(e, e.timeStamp);
562
+ if (kind === 'ignore') return;
563
+ if (kind === 'new') { carry = 0; used = 0; }
564
+ const w = wheelNotches(e, carry, ctx.o);
565
+ carry = w.carry;
566
+ const n = Math.sign(w.n) * Math.min(Math.abs(w.n), ctx.o.maxNotches - used);
567
+ if (!n) return;
568
+ used += Math.abs(n);
569
+ step(ctx, { steps: n });
416
570
  }, { passive: false });
417
571
  }
418
572
 
419
573
  function listenKeys(ctx) {
420
574
  window.addEventListener('keydown', function (e) {
421
- const dir = keyInput(e);
575
+ const input = keyInput(e);
576
+ if (!input) return;
577
+ const dir = dirFrom(ctx, input);
422
578
  if (!dir || !ctx.ours(window.scrollY, dir)) return;
423
579
  e.preventDefault();
424
- if (!e.repeat) step(ctx, dir);
580
+ if (!e.repeat) step(ctx, input);
425
581
  });
426
582
  }
427
-
428
583
  function listenTouch(ctx) {
429
584
  let y0 = null;
430
585
  window.addEventListener('touchstart', function (e) { y0 = e.touches.length === 1 ? e.touches[0].clientY : null; }, { passive: true });
@@ -438,7 +593,7 @@
438
593
  const dy = y0 - e.changedTouches[0].clientY;
439
594
  const dir = dy > 0 ? 'down' : 'up';
440
595
  y0 = null;
441
- if (Math.abs(dy) > ctx.o.swipePx && ctx.ours(window.scrollY, dir)) step(ctx, dir);
596
+ if (Math.abs(dy) > ctx.o.swipePx && ctx.ours(window.scrollY, dir)) step(ctx, { scenes: dir === 'down' ? 1 : -1 });
442
597
  }, { passive: true });
443
598
  }
444
599
 
@@ -447,10 +602,11 @@
447
602
  function skip(ctx) {
448
603
  ctx.nav.cancel();
449
604
  stopAnimation();
605
+ ctx.loader.cancel();
450
606
  const top = ctx.endTop();
451
- jump(top == null ? ctx.stops()[ctx.lastScene] : top);
607
+ jump(top == null ? ctx.stops()[ctx.lastStop] : top);
452
608
  ctx.nav.sync(window.scrollY);
453
- ctx.mark(ctx.nav.state().index);
609
+ ctx.mark();
454
610
  focusBody(ctx);
455
611
  }
456
612
 
@@ -473,30 +629,52 @@
473
629
  if (!dot && !control) return;
474
630
  e.preventDefault();
475
631
  e.stopImmediatePropagation();
476
- if (dot) return step(ctx, { to: Array.prototype.indexOf.call(dot.parentNode.children, dot) });
632
+ if (dot) return step(ctx, { to: Array.prototype.indexOf.call(dot.parentNode.children, dot), instant: true });
477
633
  return control.classList.contains('isn-skip') ? skip(ctx) : toBody(ctx);
478
634
  }, true);
479
635
  }
480
636
 
481
- // Scrolling by other means (scrollbar, find in page, links) moves the navigator's place too.
637
+ // Scrolling by other means (scrollbar, a touchpad, find in page, an anchor) moves the navigator's place
638
+ // too; a page that comes to rest off the stops and half points, above the body, settles on the nearest stop.
482
639
  function listenScroll(ctx) {
483
640
  let queued = false;
641
+ let rest = 0;
642
+ const later = function () { clearTimeout(rest); rest = setTimeout(function () { settle(ctx); }, ctx.o.snapMs); };
643
+ // while a pointer holds the page or its scrollbar, nothing settles; snapMs after it lets go, it may
644
+ window.addEventListener('pointerdown', function () { ctx.pressed = true; clearTimeout(rest); }, true);
645
+ // a press can end without a pointerup: a context menu, a release outside the window, a lost capture, the tab hidden
646
+ const release = function () { if (!ctx.pressed) return; ctx.pressed = false; later(); };
647
+ ['pointerup', 'pointercancel', 'lostpointercapture', 'contextmenu'].forEach(function (k) { window.addEventListener(k, release, true); });
648
+ // Capture catches the window losing focus; a focused element inside the page losing focus (its own blur, which
649
+ // capture also sees) must not end a press, or a click that moves focus would clear `pressed` mid-press.
650
+ window.addEventListener('blur', function (e) { if (e.target === window) release(); }, true);
651
+ document.addEventListener('visibilitychange', function () { if (document.hidden) release(); });
484
652
  window.addEventListener('scroll', function () {
485
- if (queued || ctx.nav.state().moving) return;
653
+ if (ctx.nav.state().moving) return;
654
+ later();
655
+ if (queued) return;
486
656
  queued = true;
487
657
  requestAnimationFrame(function () {
488
658
  queued = false;
489
659
  if (ctx.nav.state().moving) return;
490
660
  ctx.nav.sync(window.scrollY);
491
- ctx.mark(ctx.nav.state().index);
661
+ ctx.mark();
492
662
  });
493
663
  }, { passive: true });
494
664
  }
495
665
 
666
+ function settle(ctx) {
667
+ const y = window.scrollY;
668
+ if (ctx.held || ctx.pressed || ctx.nav.state().moving || !ctx.ours(y, 'down')) return;
669
+ const to = settleTarget(ctx.stops(), ctx.lastStop, y);
670
+ if (to >= 0) step(ctx, { to: to });
671
+ }
672
+
496
673
  const api = {
497
674
  DEFAULTS: DEFAULTS, stopPositions: stopPositions, totalWeight: totalWeight, moveDuration: moveDuration,
498
675
  wheelIgnored: wheelIgnored, createWheelGesture: createWheelGesture, belongsToIntro: belongsToIntro,
499
- createNavigator: createNavigator, keyInput: keyInput, prefetch: prefetch, attach: attach,
676
+ createNavigator: createNavigator, keyInput: keyInput, prefetch: prefetch, attach: attach, postersReady: postersReady,
677
+ gridOf: gridOf, stepFrom: stepFrom, createLoader: createLoader, sceneAt: sceneAt, wheelNotches: wheelNotches, dirOf: dirOf, settleTarget: settleTarget,
500
678
  };
501
679
  if (typeof module === 'object' && module.exports) module.exports = api;
502
680
  else window.IntroStepNav = api;