@kaydash9999/banker-plugins 0.21.0 → 0.23.0

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 (129) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +69 -0
  4. package/PRIVACY.md +8 -3
  5. package/README.md +78 -15
  6. package/bin/banker.js +154 -32
  7. package/bin/lib/codex-hooks.mjs +123 -20
  8. package/bin/lib/codex-names.mjs +4 -0
  9. package/bin/lib/codex-routes.mjs +204 -0
  10. package/bin/lib/command-skill.cjs +50 -0
  11. package/bin/lib/telemetry-config.mjs +2 -1
  12. package/codex/command-skills/front-qa/SKILL.md +56 -0
  13. package/codex/command-skills/setup/SKILL.md +87 -0
  14. package/codex/hooks.json +29 -0
  15. package/codex/manifest.json +18 -15
  16. package/codex/skills/graceful-pause/SKILL.md +2 -2
  17. package/codex/skills/progress/scripts/progress.mjs +44 -14
  18. package/codex/skills/teamwork/SKILL.md +137 -0
  19. package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
  20. package/codex/transform-matrix.md +5 -3
  21. package/commands/setup.md +2 -2
  22. package/hooks/obsidize-hook.mjs +75 -32
  23. package/hooks/telemetry-count-codex.mjs +303 -35
  24. package/hooks/update-fetch.mjs +2 -1
  25. package/hooks/update-notify.mjs +146 -13
  26. package/package.json +3 -1
  27. package/skills/3d-intro-build/SKILL.md +101 -20
  28. package/skills/3d-intro-build/references/NOTICE.md +9 -2
  29. package/skills/3d-intro-build/references/assemble.mjs +113 -22
  30. package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
  31. package/skills/3d-intro-build/references/clip-post.mjs +443 -0
  32. package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
  33. package/skills/3d-intro-build/references/intro-fixes.css +11 -0
  34. package/skills/3d-intro-build/references/prompts.md +51 -4
  35. package/skills/3d-intro-build/references/scrub-engine.js +1 -0
  36. package/skills/3d-intro-build/references/step-nav.css +29 -0
  37. package/skills/3d-intro-build/references/step-nav.js +277 -106
  38. package/skills/3d-intro-build/references/verify-intro.mjs +387 -32
  39. package/skills/3d-intro-build/references/verify.md +6 -0
  40. package/skills/3d-intro-build/references/video-pool.mjs +142 -12
  41. package/skills/3d-intro-setup/SKILL.md +20 -7
  42. package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
  43. package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
  44. package/skills/all-in-one/SKILL.md +125 -86
  45. package/skills/anti-drm/SKILL.md +1 -1
  46. package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
  47. package/skills/arch-diagram/build_pptx_template.py +47 -28
  48. package/skills/audit-web-page/SKILL.md +1 -1
  49. package/skills/audit-web-page/harness.py +0 -1
  50. package/skills/lineage/lineage.py +1435 -533
  51. package/skills/motion-graphic-make/SKILL.md +1 -1
  52. package/skills/obsidizer/SKILL.md +20 -5
  53. package/skills/omc-patch/SKILL.md +1 -1
  54. package/skills/payload-mon/SKILL.md +2 -2
  55. package/skills/play-qa/SKILL.md +7 -5
  56. package/skills/progress-mon/README.md +162 -0
  57. package/skills/progress-mon/SKILL.md +165 -0
  58. package/skills/progress-mon/assets/fixture/README.md +1 -0
  59. package/skills/progress-mon/assets/fixture/arch.json +33 -0
  60. package/skills/progress-mon/assets/fixture/commits.json +7 -0
  61. package/skills/progress-mon/assets/fixture/config.json +30 -0
  62. package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
  63. package/skills/progress-mon/assets/fixture/flow.json +44 -0
  64. package/skills/progress-mon/assets/fixture/items.json +14 -0
  65. package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
  66. package/skills/progress-mon/assets/fixture/reqs.json +11 -0
  67. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
  68. package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
  69. package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
  70. package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
  71. package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
  72. package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
  73. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
  74. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
  75. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
  76. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
  77. package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
  78. package/skills/progress-mon/references/arch.md +280 -0
  79. package/skills/progress-mon/references/authoring.md +199 -0
  80. package/skills/progress-mon/references/config.md +115 -0
  81. package/skills/progress-mon/references/flow.md +75 -0
  82. package/skills/progress-mon/references/inventory.md +58 -0
  83. package/skills/progress-mon/references/items.md +74 -0
  84. package/skills/progress-mon/references/req-spec.md +74 -0
  85. package/skills/progress-mon/references/reqs.md +30 -0
  86. package/skills/progress-mon/scripts/index.js +31 -0
  87. package/skills/progress-mon/scripts/pm-check.mjs +526 -0
  88. package/skills/progress-mon/scripts/pm-config.mjs +341 -0
  89. package/skills/progress-mon/scripts/pm-data.mjs +394 -0
  90. package/skills/progress-mon/scripts/pm-html.mjs +77 -0
  91. package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
  92. package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
  93. package/skills/progress-mon/scripts/pm-server.mjs +735 -0
  94. package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
  95. package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
  96. package/skills/ralph-qa/SKILL.md +105 -16
  97. package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
  98. package/skills/ralph-qa/references/payload-scan.mjs +19 -2
  99. package/skills/ralph-qa/references/run-folder.mjs +299 -0
  100. package/skills/ralph-qa/references/seat-payload.mjs +60 -13
  101. package/skills/ralph-qa/references/seat-run.mjs +159 -155
  102. package/skills/remains/SKILL.md +2 -1
  103. package/skills/remains/scripts/boxes.mjs +6 -2
  104. package/skills/rfp-author/scripts/md2docx.py +63 -34
  105. package/skills/setup-bypass-permissions/SKILL.md +2 -2
  106. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
  107. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
  108. package/skills/setup-mcp/SKILL.md +1 -1
  109. package/skills/setup-omc-hud/SKILL.md +2 -1
  110. package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
  111. package/skills/smart-compact/SKILL.md +1 -1
  112. package/skills/smart-compact/scripts/codex-gate.mjs +2 -1
  113. package/skills/theme-selection/SKILL.md +1 -1
  114. package/skills/tone-compact/SKILL.md +1 -1
  115. package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
  116. package/skills/ultra-chain/SKILL.md +34 -16
  117. package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
  118. package/skills/ultra-init/SKILL.md +30 -22
  119. package/skills/ultra-ui-qa/SKILL.md +1 -1
  120. package/skills/ultraqa/NOTICE.md +34 -0
  121. package/skills/ultraqa/SKILL.md +196 -0
  122. package/skills/ultraqa/references/gate-evidence.mjs +457 -0
  123. package/skills/ultraqa/references/run-folder.mjs +299 -0
  124. package/skills/ultraqa/references/tmux-drive.md +52 -0
  125. package/skills/ultraqa/references/uqa-run.mjs +339 -0
  126. package/skills/update-banker/SKILL.md +32 -7
  127. package/skills/vertical-pptx/SKILL.md +1 -1
  128. package/skills/vertical-pptx/build_a4p.js +20 -2
  129. package/skills/vertical-pptx/build_a4p.py +560 -359
@@ -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,22 @@
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
+ const KEYS = { ArrowDown: { steps: 1 }, ArrowRight: { steps: 1 }, ArrowUp: { steps: -1 }, ArrowLeft: { steps: -1 },
258
+ PageDown: { scenes: 1 }, PageUp: { scenes: -1 }, Home: { to: 0, instant: true }, End: { to: 'last', instant: true } };
211
259
 
212
- /** The step a key asks for, or null when the key is not the intro's. */
260
+ /** What a key asks for ({ steps }, { scenes }, { to }; End's to is 'last'), or null when the key is not the intro's. */
213
261
  function keyInput(e) {
214
262
  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');
263
+ if (e.key === ' ') return pressable(e.target) ? null : { scenes: e.shiftKey ? -1 : 1 };
216
264
  return KEYS[e.key] || null;
217
265
  }
218
266
 
267
+ /** The direction of an input from `base`: 'down', 'up', or null when it asks for no move. */
268
+ function dirOf(stops, base, input) {
269
+ const y = targetOf(stops, base, input);
270
+ return y == null || Math.abs(y - base) <= 1 ? null : (y > base ? 'down' : 'up');
271
+ }
272
+
219
273
  // ---- browser wiring -----------------------------------------------------------
220
274
 
221
275
  const ease = function (p) { return 0.5 - 0.5 * Math.cos(Math.PI * p); };
@@ -226,9 +280,46 @@
226
280
  try { window.scrollTo({ top: y, behavior: 'instant' }); } catch (e) { window.scrollTo(0, y); }
227
281
  }
228
282
 
283
+ // A scene a move shows paints nothing until its poster is decoded (the engine's posters decode async):
284
+ // a jump showed an empty background for 0.1-0.35 s. Decode every poster first, waiting at most capMs.
285
+ function postersReady(world, capMs, then) {
286
+ let fired = false;
287
+ const go = function () { if (!fired) { fired = true; then(); } };
288
+ const stills = Array.prototype.slice.call(world.querySelectorAll('.sw-scene__still'));
289
+ Promise.all(stills.map(function (i) { return i.decode ? i.decode().catch(function () {}) : null; })).then(go);
290
+ setTimeout(go, capMs);
291
+ }
292
+
293
+ /**
294
+ * The loading mark of the poster waits: begin(token) shows it after delayMs unless end(token) came first;
295
+ * a newer begin() or cancel() drops the older wait and its mark. show() and hide() draw it; timers injectable.
296
+ */
297
+ function createLoader(delayMs, show, hide, timers) {
298
+ // wrapped: a browser refuses setTimeout called as a method of another object (Illegal invocation)
299
+ const t = timers || { set: function (f, ms) { return setTimeout(f, ms); }, clear: function (id) { clearTimeout(id); } };
300
+ let current = null;
301
+ let timer = null;
302
+ let shown = false;
303
+ function drop() {
304
+ if (timer !== null) t.clear(timer);
305
+ timer = null;
306
+ if (shown) { shown = false; hide(); }
307
+ }
308
+ return {
309
+ begin: function (token) {
310
+ drop();
311
+ current = token;
312
+ timer = t.set(function () { timer = null; if (current === token) { shown = true; show(); } }, delayMs);
313
+ },
314
+ end: function (token) { if (token === current) { drop(); current = null; } },
315
+ cancel: function () { drop(); current = null; },
316
+ };
317
+ }
318
+
229
319
  // The running animation; a newer one, or stopAnimation(), ends it between frames.
230
320
  let animation = 0;
231
- function stopAnimation() { animation++; }
321
+ let pending = 0; // a move waiting for its posters; a newer move or stopAnimation() drops it
322
+ function stopAnimation() { animation++; pending++; }
232
323
 
233
324
  function animate(from, to, duration, done) {
234
325
  const mine = ++animation;
@@ -321,23 +412,39 @@
321
412
  document.body.insertBefore(skip, document.body.firstChild);
322
413
  document.body.appendChild(hint);
323
414
  if (o.end) document.body.appendChild(main);
324
- return { hint: hint, skip: skip, main: main };
415
+ // a status region: its text is set while a poster wait shows, so a screen reader hears it; no focus taken
416
+ const loading = element('div', 'isn-loading');
417
+ loading.setAttribute('role', 'status');
418
+ loading.setAttribute('aria-live', 'polite');
419
+ const spin = element('i', 'isn-loading__spin');
420
+ spin.setAttribute('aria-hidden', 'true');
421
+ loading.appendChild(spin);
422
+ loading.appendChild(element('span', 'isn-loading__label'));
423
+ document.body.appendChild(loading);
424
+ return { hint: hint, skip: skip, main: main, loading: loading };
325
425
  }
326
426
 
327
- /** Wires the navigation to the page. Returns { goTo(i), state(), stops(), lastScene, end }. */
427
+ /**
428
+ * Wires the navigation to the page. Returns { goTo(i), hold(on), state(), stops(), lastScene, lastStop, end };
429
+ * hold(true) stops the settling on a stop (a measuring script that parks the page between stops).
430
+ */
328
431
  function attach(world, config, options) {
329
432
  const o = Object.assign({}, DEFAULTS, options || {});
330
433
  const ctx = makeContext(world, config, o);
331
434
  ctx.ui = buildUi(o);
435
+ const label = ctx.ui.loading.querySelector('.isn-loading__label');
436
+ ctx.loader = createLoader(o.loadingDelayMs,
437
+ function () { label.textContent = o.loadingLabel; ctx.ui.loading.classList.add('is-on'); },
438
+ function () { ctx.ui.loading.classList.remove('is-on'); label.textContent = ''; });
332
439
  listenWheel(ctx);
333
440
  listenKeys(ctx);
334
441
  listenTouch(ctx);
335
442
  listenClicks(ctx);
336
443
  listenScroll(ctx);
337
444
  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 };
445
+ ctx.mark();
446
+ const handle = { goTo: function (i) { step(ctx, { to: i }); }, hold: function (on) { ctx.held = Boolean(on); },
447
+ state: function () { return ctx.nav.state(); }, stops: ctx.stops, lastScene: ctx.lastScene, lastStop: ctx.lastStop, end: o.end };
341
448
  // The navigation on this page, for scripts and for verify-intro.mjs.
342
449
  window.IntroStepNav.current = handle;
343
450
  return handle;
@@ -346,7 +453,7 @@
346
453
  function makeContext(world, config, o) {
347
454
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
348
455
  const lastScene = (config.sections || []).length - 1;
349
- const ctx = { o: o, world: world, lastScene: lastScene, focusEnd: false, warned: false };
456
+ const ctx = { o: o, world: world, config: config, lastScene: lastScene, lastStop: lastScene + 1, focusEnd: false, warned: false };
350
457
  ctx.endEl = function () { return endElement(ctx); };
351
458
  ctx.endTop = function () { const e = ctx.endEl(); return e ? Math.round(e.getBoundingClientRect().top + window.scrollY) : null; };
352
459
  // The engine sizes its track (segments + one screen) from the height it last laid out at, so the
@@ -357,11 +464,24 @@
357
464
  return h > 0 ? h / (totalWeight(config) + 1) : window.innerHeight;
358
465
  };
359
466
  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); } });
467
+ ctx.ours = function (y, dir) { return belongsToIntro(y, dir, ctx.stops()[ctx.lastStop], ctx.endTop()); };
468
+ ctx.mark = function () { mark(ctx); };
469
+ ctx.nav = createNavigator({ stops: ctx.stops, lastStop: ctx.lastStop, where: function () { return window.scrollY; },
470
+ onLeave: function (instant) { instantCopy(instant); showCopy(ctx, -1); },
471
+ onArrive: function () { ctx.mark(); requestAnimationFrame(function () { requestAnimationFrame(function () { instantCopy(false); }); }); },
472
+ move: function (from, to, done, instant) {
473
+ const mine = ++pending;
474
+ // an instant move swaps the copy at once even when it retargets a move already under way (no onLeave then)
475
+ instantCopy(instant);
476
+ ctx.loader.begin(mine);
477
+ // from where the page is once the posters are ready: an earlier move may have run on while waiting
478
+ postersReady(world, instant ? o.instantDecodeCapMs : o.decodeCapMs, function () {
479
+ if (mine !== pending) return;
480
+ ctx.loader.end(mine);
481
+ const at = window.scrollY;
482
+ animate(at, to, instant ? 0 : moveDuration(to - at, ctx.vh(), o, reduce), done);
483
+ });
484
+ } });
365
485
  const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
366
486
  document.documentElement.classList.add('isn-on', fine ? 'isn-mouse' : 'isn-touch');
367
487
  return ctx;
@@ -387,44 +507,75 @@
387
507
  e.focus({ preventScroll: true });
388
508
  }
389
509
 
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) {
510
+ // An instant move (Home, End, a dot) swaps the copy without the fade (class isn-instant, step-nav.css).
511
+ function instantCopy(on) { document.documentElement.classList.toggle('isn-instant', Boolean(on)); }
512
+
513
+ // The copy of scene k shows over the engine's own fade (class isn-copy, see step-nav.css); -1 shows none.
514
+ // The same element: nothing is copied, so a screen reader and the focus meet each text once.
515
+ function showCopy(ctx, k) {
516
+ ctx.world.querySelectorAll('.sw-copy').forEach(function (c, i) { c.classList.toggle('isn-copy', i === k); });
517
+ }
518
+
519
+ // Classes the CSS reads: isn-last at the last scene's end; isn-ready there once its clip has played
520
+ // to the end (under reduced motion that stop is reached at once and the engine plays no clip);
521
+ // isn-body at or below the body top. At a stop or a half point the copy in view shows. Moves the
522
+ // focus to the body after the main button.
523
+ function mark(ctx) {
395
524
  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);
525
+ const y = window.scrollY;
526
+ const stops = ctx.stops();
527
+ const atLast = Math.abs(y - stops[ctx.lastStop]) <= 1;
528
+ const body = stops.length - 1 > ctx.lastStop && y >= stops[stops.length - 1] - 1;
529
+ c.toggle('isn-last', atLast);
530
+ c.toggle('isn-ready', Boolean(ctx.endEl()) && atLast);
531
+ c.toggle('isn-body', body);
532
+ const onGrid = gridOf(stops).some(function (v) { return Math.abs(v - y) <= 1; });
533
+ showCopy(ctx, onGrid && !body ? sceneAt(ctx.config, ctx.vh(), y) : -1);
534
+ if (ctx.focusEnd && body) focusBody(ctx);
401
535
  }
402
536
 
403
- function step(ctx, dir) {
537
+ // An input ({ steps }, { scenes }, { to }, End's { to: 'last' }; instant: no animation) from where the page stands.
538
+ function step(ctx, input) {
404
539
  ctx.nav.sync(window.scrollY);
405
- ctx.nav.input(dir);
540
+ ctx.nav.input(input.to === 'last' ? { to: ctx.lastStop, instant: input.instant } : input);
406
541
  }
407
542
 
543
+ // The direction an input takes from where the page (or the move in flight) is going.
544
+ const dirFrom = function (ctx, input) {
545
+ const st = ctx.nav.state();
546
+ return dirOf(ctx.stops(), st.moving ? st.y : window.scrollY, input.to === 'last' ? { to: ctx.lastStop, instant: input.instant } : input);
547
+ };
548
+
408
549
  function listenWheel(ctx) {
409
550
  const gesture = createWheelGesture(ctx.o);
551
+ let carry = 0;
552
+ let used = 0;
410
553
  window.addEventListener('wheel', function (e) {
411
554
  if (wheelIgnored(e)) return;
412
- const dir = e.deltaY > 0 ? 'down' : 'up';
413
- if (!ctx.ours(window.scrollY, dir)) return;
555
+ if (!ctx.ours(window.scrollY, e.deltaY > 0 ? 'down' : 'up')) return;
414
556
  e.preventDefault();
415
- if (gesture.feed(e, e.timeStamp) === 'new') step(ctx, dir);
557
+ const kind = gesture.feed(e, e.timeStamp);
558
+ if (kind === 'ignore') return;
559
+ if (kind === 'new') { carry = 0; used = 0; }
560
+ const w = wheelNotches(e, carry, ctx.o);
561
+ carry = w.carry;
562
+ const n = Math.sign(w.n) * Math.min(Math.abs(w.n), ctx.o.maxNotches - used);
563
+ if (!n) return;
564
+ used += Math.abs(n);
565
+ step(ctx, { steps: n });
416
566
  }, { passive: false });
417
567
  }
418
568
 
419
569
  function listenKeys(ctx) {
420
570
  window.addEventListener('keydown', function (e) {
421
- const dir = keyInput(e);
571
+ const input = keyInput(e);
572
+ if (!input) return;
573
+ const dir = dirFrom(ctx, input);
422
574
  if (!dir || !ctx.ours(window.scrollY, dir)) return;
423
575
  e.preventDefault();
424
- if (!e.repeat) step(ctx, dir);
576
+ if (!e.repeat) step(ctx, input);
425
577
  });
426
578
  }
427
-
428
579
  function listenTouch(ctx) {
429
580
  let y0 = null;
430
581
  window.addEventListener('touchstart', function (e) { y0 = e.touches.length === 1 ? e.touches[0].clientY : null; }, { passive: true });
@@ -438,7 +589,7 @@
438
589
  const dy = y0 - e.changedTouches[0].clientY;
439
590
  const dir = dy > 0 ? 'down' : 'up';
440
591
  y0 = null;
441
- if (Math.abs(dy) > ctx.o.swipePx && ctx.ours(window.scrollY, dir)) step(ctx, dir);
592
+ if (Math.abs(dy) > ctx.o.swipePx && ctx.ours(window.scrollY, dir)) step(ctx, { scenes: dir === 'down' ? 1 : -1 });
442
593
  }, { passive: true });
443
594
  }
444
595
 
@@ -447,10 +598,11 @@
447
598
  function skip(ctx) {
448
599
  ctx.nav.cancel();
449
600
  stopAnimation();
601
+ ctx.loader.cancel();
450
602
  const top = ctx.endTop();
451
- jump(top == null ? ctx.stops()[ctx.lastScene] : top);
603
+ jump(top == null ? ctx.stops()[ctx.lastStop] : top);
452
604
  ctx.nav.sync(window.scrollY);
453
- ctx.mark(ctx.nav.state().index);
605
+ ctx.mark();
454
606
  focusBody(ctx);
455
607
  }
456
608
 
@@ -473,30 +625,49 @@
473
625
  if (!dot && !control) return;
474
626
  e.preventDefault();
475
627
  e.stopImmediatePropagation();
476
- if (dot) return step(ctx, { to: Array.prototype.indexOf.call(dot.parentNode.children, dot) });
628
+ if (dot) return step(ctx, { to: Array.prototype.indexOf.call(dot.parentNode.children, dot), instant: true });
477
629
  return control.classList.contains('isn-skip') ? skip(ctx) : toBody(ctx);
478
630
  }, true);
479
631
  }
480
632
 
481
- // Scrolling by other means (scrollbar, find in page, links) moves the navigator's place too.
633
+ // Scrolling by other means (scrollbar, a touchpad, find in page, an anchor) moves the navigator's place
634
+ // too; a page that comes to rest off the stops and half points, above the body, settles on the nearest stop.
482
635
  function listenScroll(ctx) {
483
636
  let queued = false;
637
+ let rest = 0;
638
+ const later = function () { clearTimeout(rest); rest = setTimeout(function () { settle(ctx); }, ctx.o.snapMs); };
639
+ // while a pointer holds the page or its scrollbar, nothing settles; snapMs after it lets go, it may
640
+ window.addEventListener('pointerdown', function () { ctx.pressed = true; clearTimeout(rest); }, true);
641
+ // a press can end without a pointerup: a context menu, a release outside the window, a lost capture, the tab hidden
642
+ const release = function () { if (!ctx.pressed) return; ctx.pressed = false; later(); };
643
+ ['pointerup', 'pointercancel', 'lostpointercapture', 'contextmenu', 'blur'].forEach(function (k) { window.addEventListener(k, release, true); });
644
+ document.addEventListener('visibilitychange', function () { if (document.hidden) release(); });
484
645
  window.addEventListener('scroll', function () {
485
- if (queued || ctx.nav.state().moving) return;
646
+ if (ctx.nav.state().moving) return;
647
+ later();
648
+ if (queued) return;
486
649
  queued = true;
487
650
  requestAnimationFrame(function () {
488
651
  queued = false;
489
652
  if (ctx.nav.state().moving) return;
490
653
  ctx.nav.sync(window.scrollY);
491
- ctx.mark(ctx.nav.state().index);
654
+ ctx.mark();
492
655
  });
493
656
  }, { passive: true });
494
657
  }
495
658
 
659
+ function settle(ctx) {
660
+ const y = window.scrollY;
661
+ if (ctx.held || ctx.pressed || ctx.nav.state().moving || !ctx.ours(y, 'down')) return;
662
+ const to = settleTarget(ctx.stops(), ctx.lastStop, y);
663
+ if (to >= 0) step(ctx, { to: to });
664
+ }
665
+
496
666
  const api = {
497
667
  DEFAULTS: DEFAULTS, stopPositions: stopPositions, totalWeight: totalWeight, moveDuration: moveDuration,
498
668
  wheelIgnored: wheelIgnored, createWheelGesture: createWheelGesture, belongsToIntro: belongsToIntro,
499
- createNavigator: createNavigator, keyInput: keyInput, prefetch: prefetch, attach: attach,
669
+ createNavigator: createNavigator, keyInput: keyInput, prefetch: prefetch, attach: attach, postersReady: postersReady,
670
+ gridOf: gridOf, stepFrom: stepFrom, createLoader: createLoader, sceneAt: sceneAt, wheelNotches: wheelNotches, dirOf: dirOf, settleTarget: settleTarget,
500
671
  };
501
672
  if (typeof module === 'object' && module.exports) module.exports = api;
502
673
  else window.IntroStepNav = api;