@kaydash9999/banker-plugins 0.15.1 → 0.16.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 (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +60 -0
  4. package/README.md +31 -9
  5. package/codex/manifest.json +6 -3
  6. package/codex/transform-matrix.md +2 -0
  7. package/commands/progress.md +10 -0
  8. package/commands/setup.md +1 -1
  9. package/hooks/graceful-pause.mjs +31 -71
  10. package/hooks/hooks.json +11 -1
  11. package/hooks/progress-fallback.mjs +10 -0
  12. package/hooks/progress.mjs +427 -0
  13. package/hooks/register.mjs +82 -5
  14. package/package.json +1 -1
  15. package/skills/3d-intro-build/SKILL.md +235 -122
  16. package/skills/3d-intro-build/references/NOTICE.md +2 -1
  17. package/skills/3d-intro-build/references/assemble.mjs +164 -108
  18. package/skills/3d-intro-build/references/azure-adapter.mjs +115 -29
  19. package/skills/3d-intro-build/references/curate.mjs +573 -267
  20. package/skills/3d-intro-build/references/intro-fixes.css +61 -0
  21. package/skills/3d-intro-build/references/panel-glass.css +55 -0
  22. package/skills/3d-intro-build/references/preview-lib.mjs +256 -0
  23. package/skills/3d-intro-build/references/prompts.md +32 -9
  24. package/skills/3d-intro-build/references/serve.mjs +111 -103
  25. package/skills/3d-intro-build/references/step-nav.css +59 -0
  26. package/skills/3d-intro-build/references/step-nav.js +503 -0
  27. package/skills/3d-intro-build/references/verify-intro.mjs +474 -0
  28. package/skills/3d-intro-build/references/verify.md +45 -0
  29. package/skills/3d-intro-build/references/video-pool.mjs +780 -0
  30. package/skills/3d-intro-setup/SKILL.md +49 -12
  31. package/skills/3d-intro-setup/references/azure-adapter.mjs +115 -29
  32. package/skills/3d-intro-setup/references/video-pool.mjs +780 -0
  33. package/skills/lineage/SKILL.md +12 -8
  34. package/skills/lineage/lineage.py +190 -59
  35. package/skills/motion-graphic-make/SKILL.md +73 -28
  36. package/skills/motion-graphic-make/references/curate.mjs +607 -0
  37. package/skills/motion-graphic-make/references/preview-lib.mjs +256 -0
  38. package/skills/motion-graphic-make/references/serve.mjs +131 -0
  39. package/skills/omc-patch/SKILL.md +143 -0
  40. package/skills/omc-patch/scripts/omc-patch.mjs +577 -0
  41. package/skills/remains/SKILL.md +125 -0
  42. package/skills/remains/scripts/boxes.mjs +448 -0
  43. package/skills/trouble-shooting/SKILL.md +50 -0
@@ -0,0 +1,503 @@
1
+ /*
2
+ * Scene-by-scene navigation for the scroll-world intro (opt-in: intro.json "stepNav").
3
+ *
4
+ * 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.
11
+ *
12
+ * Options (intro.json "stepNav", all optional; values are the defaults below):
13
+ * end selector of the body start; omit on a standalone intro page. It is looked up at
14
+ * each input, so a body drawn after this script still counts; while it is missing
15
+ * the page scrolls on past the last scene as usual (a warning is logged once)
16
+ * gestureGapMs a wheel pause longer than this, or a turn of direction, starts a new gesture
17
+ * minWheel smaller |deltaY| is left to the browser (so are ctrl+wheel zoom and sideways wheels)
18
+ * swipePx a touch swipe longer than this is one step
19
+ * durBaseMs, durPerVhMs, durMaxMs a move takes min(durMax, durBase + distance in screen heights
20
+ * 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
23
+ * prefetch download the clips at load so the first move already plays video; on a phone or
24
+ * with Save-Data only the first three in playback order
25
+ * hintLabel, skipLabel, mainLabel texts of the mouse hint, the skip link and the main button
26
+ * The main button shows once the last clip has played to its end (the page stands at the last
27
+ * stop; at once under reduced motion, where the engine loads no clips) and only with a body. The
28
+ * skip link jumps without animation and moves the focus to the body.
29
+ *
30
+ * Plain script (no module): sets window.IntroStepNav (and IntroStepNav.current, the navigation of
31
+ * this page, after attach); under Node it exports the same functions so the logic is tested without
32
+ * a browser (step-nav.test.mjs).
33
+ */
34
+ 'use strict';
35
+ // A block, not a function: its declarations stay out of the global scope (strict mode).
36
+ {
37
+
38
+ const DEFAULTS = Object.freeze({
39
+ end: null, gestureGapMs: 220, minWheel: 2, swipePx: 30,
40
+ durBaseMs: 2600, durPerVhMs: 760, durMaxMs: 5600, lastGuardMs: 250, prefetch: true,
41
+ hintLabel: 'Scroll or press the down key', skipLabel: 'Skip intro', mainLabel: 'Go to main content',
42
+ });
43
+
44
+ // ---- logic (tested under Node) ----------------------------------------------
45
+
46
+ // The engine's segments in screen heights: a dive per section, then a connector where one exists.
47
+ function spansOf(config) {
48
+ const sections = config.sections || [];
49
+ const connectors = config.connectors || [];
50
+ const dive = config.diveScroll || 1.3;
51
+ const conn = config.connScroll || 0.9;
52
+ let off = 0;
53
+ const spans = sections.map(function (s, i) {
54
+ const start = off;
55
+ off += s.scroll || dive;
56
+ const span = [start, off];
57
+ if (i < sections.length - 1 && connectors[i]) off += conn;
58
+ return span;
59
+ });
60
+ return { spans: spans, total: off };
61
+ }
62
+
63
+ /** The scroll length of the segments in screen heights (the engine's track adds one screen). */
64
+ function totalWeight(config) { return spansOf(config).total; }
65
+
66
+ /** Stop positions in px: the first scene at 0, middle scenes mid-dive, the last at its end, then the body. */
67
+ function stopPositions(config, vh, endTop) {
68
+ 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
+ });
73
+ return endTop == null ? stops : stops.concat([endTop]);
74
+ }
75
+
76
+ function moveDuration(distance, vh, o, reduce) {
77
+ if (reduce) return 0;
78
+ return Math.min(o.durMaxMs, o.durBaseMs + (Math.abs(distance) / vh) * o.durPerVhMs);
79
+ }
80
+
81
+ /**
82
+ * Zoom (ctrl+wheel, pinch) and sideways wheels stay with the browser. A tiny wheel is held by the
83
+ * intro but starts no gesture (an inertia tail would otherwise nudge the page off its stop).
84
+ */
85
+ function wheelIgnored(e) {
86
+ return Boolean(e.ctrlKey) || Math.abs(e.deltaX || 0) > Math.abs(e.deltaY || 0);
87
+ }
88
+
89
+ /** Splits a wheel stream into gestures: feed(event, ms) is 'new', 'same' or 'ignore'. */
90
+ function createWheelGesture(o) {
91
+ let st = null;
92
+ function reset(dir, mag, t) {
93
+ st = { dir: dir, last: t, peak: mag, min: mag, falling: false };
94
+ return 'new';
95
+ }
96
+ function falling(mag, t) {
97
+ if (mag > Math.max(st.min * 2, st.min + 8)) return reset(st.dir, mag, t);
98
+ st.min = Math.min(st.min, mag);
99
+ return 'same';
100
+ }
101
+ function rising(mag) {
102
+ if (mag < st.peak / 2) {
103
+ st.falling = true;
104
+ st.min = mag;
105
+ } else st.peak = Math.max(st.peak, mag);
106
+ return 'same';
107
+ }
108
+ return {
109
+ feed: function (e, t) {
110
+ const mag = Math.abs(e.deltaY || 0);
111
+ if (wheelIgnored(e) || mag < o.minWheel) return 'ignore';
112
+ const dir = Math.sign(e.deltaY);
113
+ if (!st || dir !== st.dir || t - st.last > o.gestureGapMs) return reset(dir, mag, t);
114
+ st.last = t;
115
+ return st.falling ? falling(mag, t) : rising(mag);
116
+ },
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Whether an input belongs to the intro (else the browser scrolls): above the body top, or an up
122
+ * input at it. Without a body element (none, or not drawn yet) the page scrolls on past the last
123
+ * scene, so nothing below the intro is ever trapped.
124
+ */
125
+ function belongsToIntro(y, dir, lastStop, endTop) {
126
+ const edge = endTop == null ? lastStop : endTop;
127
+ return y < edge - 1 || (Math.abs(y - edge) <= 1 && dir === 'up');
128
+ }
129
+
130
+ const nearest = function (list, y) {
131
+ let best = 0;
132
+ list.forEach(function (v, i) { if (Math.abs(v - y) < Math.abs(list[best] - y)) best = i; });
133
+ return best;
134
+ };
135
+
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;
143
+ }
144
+
145
+ /**
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.
149
+ */
150
+ function createNavigator(opt) {
151
+ let index = 0;
152
+ let pos = 0;
153
+ let target = -1;
154
+ let queued = null;
155
+ let guardUntil = -Infinity;
156
+ let generation = 0;
157
+ 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); });
176
+ }
177
+ 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
+ },
194
+ state: function () {
195
+ const n = list().length;
196
+ return { index: index, moving: target !== -1, queued: queued, atBody: n - 1 > opt.lastScene && index === n - 1 };
197
+ },
198
+ };
199
+ }
200
+
201
+ // Fields and composite widgets keep every key; a focused button or link keeps Space (it presses).
202
+ const role = function (t) { return (t && t.getAttribute && t.getAttribute('role')) || ''; };
203
+ const editable = function (t) {
204
+ return Boolean(t) && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName || '')
205
+ || /^(menu|menubar|listbox|slider|spinbutton|tablist|grid|tree|treegrid|combobox|textbox|radiogroup)$/.test(role(t)));
206
+ };
207
+ const pressable = function (t) {
208
+ return Boolean(t) && (/^(BUTTON|A|SUMMARY)$/.test(t.tagName || '') || /^(button|link|checkbox|switch|menuitem|tab|option)$/.test(role(t)));
209
+ };
210
+ const KEYS = { ArrowDown: 'down', PageDown: 'down', ArrowUp: 'up', PageUp: 'up' };
211
+
212
+ /** The step a key asks for, or null when the key is not the intro's. */
213
+ function keyInput(e) {
214
+ 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');
216
+ return KEYS[e.key] || null;
217
+ }
218
+
219
+ // ---- browser wiring -----------------------------------------------------------
220
+
221
+ const ease = function (p) { return 0.5 - 0.5 * Math.cos(Math.PI * p); };
222
+
223
+ // Sets the scroll position at once ('instant' beats a site's scroll-behavior: smooth; an older
224
+ // browser that does not know it takes the two-number form).
225
+ function jump(y) {
226
+ try { window.scrollTo({ top: y, behavior: 'instant' }); } catch (e) { window.scrollTo(0, y); }
227
+ }
228
+
229
+ // The running animation; a newer one, or stopAnimation(), ends it between frames.
230
+ let animation = 0;
231
+ function stopAnimation() { animation++; }
232
+
233
+ function animate(from, to, duration, done) {
234
+ const mine = ++animation;
235
+ if (duration <= 0) {
236
+ jump(to);
237
+ return done();
238
+ }
239
+ const t0 = performance.now();
240
+ (function frame(t) {
241
+ if (mine !== animation) return;
242
+ const p = Math.min(1, (t - t0) / duration);
243
+ jump(from + (to - from) * ease(p));
244
+ if (p < 1) requestAnimationFrame(frame);
245
+ else done();
246
+ })(t0);
247
+ }
248
+
249
+ const isMobile = function () {
250
+ return window.matchMedia('(hover: none) and (pointer: coarse)').matches || window.matchMedia('(max-width: 860px)').matches;
251
+ };
252
+
253
+ /** The clip URLs the engine will fetch on this device, absolute, in playback order. */
254
+ function clipUrls(config) {
255
+ const m = isMobile();
256
+ const sections = config.sections || [];
257
+ const conns = config.connectors || [];
258
+ const connsM = config.connectorsMobile || [];
259
+ const urls = [];
260
+ sections.forEach(function (s, i) {
261
+ urls.push((m && s.clipMobile) || s.clip);
262
+ if (i < sections.length - 1) urls.push((m && connsM[i]) || conns[i]);
263
+ });
264
+ return urls.filter(Boolean).map(function (u) { return new URL(u, location.href).href; });
265
+ }
266
+
267
+ const absolute = function (input) {
268
+ try {
269
+ const raw = typeof input === 'string' ? input : input && input.url;
270
+ return raw ? new URL(raw, location.href).href : '';
271
+ } catch (e) { return ''; }
272
+ };
273
+
274
+ /**
275
+ * Starts the clip downloads now and serves the engine's later fetch of the same URL from them, so
276
+ * one URL is fetched once. On a phone or with Save-Data only the first three clips start early.
277
+ * Does nothing under reduced motion (the engine loads no clips then).
278
+ */
279
+ function prefetch(config, options) {
280
+ const o = Object.assign({}, DEFAULTS, options || {});
281
+ if (!o.prefetch || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
282
+ const all = clipUrls(config);
283
+ const urls = new Set(all);
284
+ const cache = new Map();
285
+ const original = window.fetch.bind(window);
286
+ const blobOf = function (url) {
287
+ if (!cache.has(url)) {
288
+ cache.set(url, original(url).then(function (r) {
289
+ if (!r.ok) throw new Error('HTTP ' + r.status);
290
+ return r.blob();
291
+ }).catch(function (e) { cache.delete(url); throw e; }));
292
+ }
293
+ return cache.get(url);
294
+ };
295
+ window.fetch = function (input, init) {
296
+ const url = absolute(input);
297
+ if (!urls.has(url) || (init && init.method && init.method !== 'GET')) return original(input, init);
298
+ return blobOf(url).then(function (b) { return new Response(b, { status: 200, headers: { 'Content-Type': b.type } }); });
299
+ };
300
+ const lean = isMobile() || Boolean(navigator.connection && navigator.connection.saveData);
301
+ (lean ? all.slice(0, 3) : all).forEach(function (u) { blobOf(u).catch(function () {}); });
302
+ }
303
+
304
+ function element(tag, cls, text) {
305
+ const e = document.createElement(tag);
306
+ e.className = cls;
307
+ if (text) e.textContent = text;
308
+ return e;
309
+ }
310
+
311
+ function buildUi(o) {
312
+ const hint = element('div', 'isn-hint');
313
+ hint.setAttribute('aria-hidden', 'true');
314
+ hint.appendChild(element('i', 'isn-hint__wheel'));
315
+ hint.appendChild(element('i', 'isn-hint__key'));
316
+ hint.appendChild(element('span', '', o.hintLabel));
317
+ const skip = element('a', 'isn-skip', o.skipLabel);
318
+ skip.href = /^#[A-Za-z][\w-]*$/.test(o.end || '') ? o.end : '#';
319
+ const main = element('button', 'isn-main', o.mainLabel);
320
+ main.type = 'button';
321
+ document.body.insertBefore(skip, document.body.firstChild);
322
+ document.body.appendChild(hint);
323
+ if (o.end) document.body.appendChild(main);
324
+ return { hint: hint, skip: skip, main: main };
325
+ }
326
+
327
+ /** Wires the navigation to the page. Returns { goTo(i), state(), stops(), lastScene, end }. */
328
+ function attach(world, config, options) {
329
+ const o = Object.assign({}, DEFAULTS, options || {});
330
+ const ctx = makeContext(world, config, o);
331
+ ctx.ui = buildUi(o);
332
+ listenWheel(ctx);
333
+ listenKeys(ctx);
334
+ listenTouch(ctx);
335
+ listenClicks(ctx);
336
+ listenScroll(ctx);
337
+ 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 };
341
+ // The navigation on this page, for scripts and for verify-intro.mjs.
342
+ window.IntroStepNav.current = handle;
343
+ return handle;
344
+ }
345
+
346
+ function makeContext(world, config, o) {
347
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
348
+ const lastScene = (config.sections || []).length - 1;
349
+ const ctx = { o: o, world: world, lastScene: lastScene, focusEnd: false, warned: false };
350
+ ctx.endEl = function () { return endElement(ctx); };
351
+ ctx.endTop = function () { const e = ctx.endEl(); return e ? Math.round(e.getBoundingClientRect().top + window.scrollY) : null; };
352
+ // The engine sizes its track (segments + one screen) from the height it last laid out at, so the
353
+ // stops follow that height, not a URL-bar resize the engine ignored.
354
+ ctx.vh = function () {
355
+ const track = world.querySelector('.sw-track');
356
+ const h = track ? track.getBoundingClientRect().height : 0;
357
+ return h > 0 ? h / (totalWeight(config) + 1) : window.innerHeight;
358
+ };
359
+ 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); } });
365
+ const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
366
+ document.documentElement.classList.add('isn-on', fine ? 'isn-mouse' : 'isn-touch');
367
+ return ctx;
368
+ }
369
+
370
+ // The body element, looked up each time; a configured one that is missing is logged once.
371
+ function endElement(ctx) {
372
+ if (!ctx.o.end) return null;
373
+ const e = document.querySelector(ctx.o.end);
374
+ // A body drawn after this script is a supported layout: warn only once the page has loaded.
375
+ if (!e && !ctx.warned && document.readyState === 'complete') {
376
+ ctx.warned = true;
377
+ console.warn('IntroStepNav: no element matches ' + ctx.o.end + '; the page scrolls on past the last scene until it exists');
378
+ }
379
+ return e;
380
+ }
381
+
382
+ function focusBody(ctx) {
383
+ const e = ctx.endEl();
384
+ ctx.focusEnd = false;
385
+ if (!e) return;
386
+ if (!e.hasAttribute('tabindex')) e.setAttribute('tabindex', '-1');
387
+ e.focus({ preventScroll: true });
388
+ }
389
+
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) {
395
+ 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);
401
+ }
402
+
403
+ function step(ctx, dir) {
404
+ ctx.nav.sync(window.scrollY);
405
+ ctx.nav.input(dir);
406
+ }
407
+
408
+ function listenWheel(ctx) {
409
+ const gesture = createWheelGesture(ctx.o);
410
+ window.addEventListener('wheel', function (e) {
411
+ if (wheelIgnored(e)) return;
412
+ const dir = e.deltaY > 0 ? 'down' : 'up';
413
+ if (!ctx.ours(window.scrollY, dir)) return;
414
+ e.preventDefault();
415
+ if (gesture.feed(e, e.timeStamp) === 'new') step(ctx, dir);
416
+ }, { passive: false });
417
+ }
418
+
419
+ function listenKeys(ctx) {
420
+ window.addEventListener('keydown', function (e) {
421
+ const dir = keyInput(e);
422
+ if (!dir || !ctx.ours(window.scrollY, dir)) return;
423
+ e.preventDefault();
424
+ if (!e.repeat) step(ctx, dir);
425
+ });
426
+ }
427
+
428
+ function listenTouch(ctx) {
429
+ let y0 = null;
430
+ window.addEventListener('touchstart', function (e) { y0 = e.touches.length === 1 ? e.touches[0].clientY : null; }, { passive: true });
431
+ window.addEventListener('touchmove', function (e) {
432
+ if (y0 === null) return;
433
+ const dir = y0 - e.touches[0].clientY > 0 ? 'down' : 'up';
434
+ if (ctx.ours(window.scrollY, dir)) e.preventDefault();
435
+ }, { passive: false });
436
+ window.addEventListener('touchend', function (e) {
437
+ if (y0 === null) return;
438
+ const dy = y0 - e.changedTouches[0].clientY;
439
+ const dir = dy > 0 ? 'down' : 'up';
440
+ y0 = null;
441
+ if (Math.abs(dy) > ctx.o.swipePx && ctx.ours(window.scrollY, dir)) step(ctx, dir);
442
+ }, { passive: true });
443
+ }
444
+
445
+ // The skip link jumps at once (no animation to wait through) and hands the focus to the body; on a
446
+ // standalone page it goes to the last scene.
447
+ function skip(ctx) {
448
+ ctx.nav.cancel();
449
+ stopAnimation();
450
+ const top = ctx.endTop();
451
+ jump(top == null ? ctx.stops()[ctx.lastScene] : top);
452
+ ctx.nav.sync(window.scrollY);
453
+ ctx.mark(ctx.nav.state().index);
454
+ focusBody(ctx);
455
+ }
456
+
457
+ // The main button moves to the body (focus follows on arrival), or focuses it when already there.
458
+ function toBody(ctx) {
459
+ ctx.nav.sync(window.scrollY);
460
+ const st = ctx.nav.state();
461
+ if (st.atBody) return focusBody(ctx);
462
+ ctx.focusEnd = Boolean(ctx.endEl());
463
+ ctx.nav.input({ to: ctx.stops().length - 1 });
464
+ }
465
+
466
+ // Route dots and nav items go through the navigator, not the engine's own smooth scroll.
467
+ function listenClicks(ctx) {
468
+ document.addEventListener('click', function (e) {
469
+ const at = function (sel) { return e.target.closest ? e.target.closest(sel) : null; };
470
+ const hit = at('.sw-route__dot, .sw-nav__item');
471
+ const dot = hit && ctx.world.contains(hit) ? hit : null;
472
+ const control = at('.isn-skip, .isn-main');
473
+ if (!dot && !control) return;
474
+ e.preventDefault();
475
+ e.stopImmediatePropagation();
476
+ if (dot) return step(ctx, { to: Array.prototype.indexOf.call(dot.parentNode.children, dot) });
477
+ return control.classList.contains('isn-skip') ? skip(ctx) : toBody(ctx);
478
+ }, true);
479
+ }
480
+
481
+ // Scrolling by other means (scrollbar, find in page, links) moves the navigator's place too.
482
+ function listenScroll(ctx) {
483
+ let queued = false;
484
+ window.addEventListener('scroll', function () {
485
+ if (queued || ctx.nav.state().moving) return;
486
+ queued = true;
487
+ requestAnimationFrame(function () {
488
+ queued = false;
489
+ if (ctx.nav.state().moving) return;
490
+ ctx.nav.sync(window.scrollY);
491
+ ctx.mark(ctx.nav.state().index);
492
+ });
493
+ }, { passive: true });
494
+ }
495
+
496
+ const api = {
497
+ DEFAULTS: DEFAULTS, stopPositions: stopPositions, totalWeight: totalWeight, moveDuration: moveDuration,
498
+ wheelIgnored: wheelIgnored, createWheelGesture: createWheelGesture, belongsToIntro: belongsToIntro,
499
+ createNavigator: createNavigator, keyInput: keyInput, prefetch: prefetch, attach: attach,
500
+ };
501
+ if (typeof module === 'object' && module.exports) module.exports = api;
502
+ else window.IntroStepNav = api;
503
+ }