@stacknav/core 0.3.1 → 0.4.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 (56) hide show
  1. package/README.md +111 -59
  2. package/dist/animate.d.ts +18 -0
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +44 -2
  5. package/dist/animate.js.map +1 -1
  6. package/dist/css-vars.d.ts +3 -3
  7. package/dist/css-vars.d.ts.map +1 -1
  8. package/dist/css-vars.js +47 -4
  9. package/dist/css-vars.js.map +1 -1
  10. package/dist/history-adapter.d.ts +0 -1
  11. package/dist/history-adapter.d.ts.map +1 -1
  12. package/dist/history-adapter.js +1 -5
  13. package/dist/history-adapter.js.map +1 -1
  14. package/dist/index.d.ts +27 -18
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +38 -15
  17. package/dist/index.js.map +1 -1
  18. package/dist/native-transition.d.ts +93 -0
  19. package/dist/native-transition.d.ts.map +1 -0
  20. package/dist/{ios-transition.js → native-transition.js} +42 -25
  21. package/dist/native-transition.js.map +1 -0
  22. package/dist/navigation-stack.d.ts +3 -0
  23. package/dist/navigation-stack.d.ts.map +1 -1
  24. package/dist/navigation-stack.js +42 -3
  25. package/dist/navigation-stack.js.map +1 -1
  26. package/dist/platform.d.ts +6 -0
  27. package/dist/platform.d.ts.map +1 -0
  28. package/dist/platform.js +22 -0
  29. package/dist/platform.js.map +1 -0
  30. package/dist/stacknav.css +4 -13
  31. package/dist/styles.d.ts +5 -9
  32. package/dist/styles.d.ts.map +1 -1
  33. package/dist/styles.js +6 -12
  34. package/dist/styles.js.map +1 -1
  35. package/dist/swipe-back.d.ts +4 -0
  36. package/dist/swipe-back.d.ts.map +1 -0
  37. package/dist/swipe-back.js +31 -0
  38. package/dist/swipe-back.js.map +1 -0
  39. package/package.json +3 -2
  40. package/src/animate.ts +47 -4
  41. package/src/css-vars.ts +43 -4
  42. package/src/history-adapter.ts +1 -5
  43. package/src/index.ts +49 -24
  44. package/src/{ios-transition.ts → native-transition.ts} +67 -35
  45. package/src/navigation-stack.ts +30 -3
  46. package/src/platform.ts +23 -0
  47. package/src/styles.ts +6 -12
  48. package/src/swipe-back.ts +35 -0
  49. package/dist/edge-pan-gesture.d.ts +0 -36
  50. package/dist/edge-pan-gesture.d.ts.map +0 -1
  51. package/dist/edge-pan-gesture.js +0 -155
  52. package/dist/edge-pan-gesture.js.map +0 -1
  53. package/dist/ios-transition.d.ts +0 -61
  54. package/dist/ios-transition.d.ts.map +0 -1
  55. package/dist/ios-transition.js.map +0 -1
  56. package/src/edge-pan-gesture.ts +0 -190
@@ -1,20 +1,33 @@
1
1
  import { easings, prefersReducedMotion, type Easing } from './animate.ts';
2
2
  import { cssVars, parseEasing, parseNumber, parseRatio, parseTime } from './css-vars.ts';
3
3
  import type { SettleInput, StackEntry, Transition } from './navigation-stack.ts';
4
+ import { detectPlatform, type Platform } from './platform.ts';
4
5
 
5
- export interface IOSTransitionOptions {
6
+ export interface NativeTransitionOptions {
7
+ /**
8
+ * Whose push/pop to imitate. `auto` (the default) asks the browser and
9
+ * falls back to `ios`. It picks the defaults for everything below; an
10
+ * option given explicitly wins over the platform's, as a CSS variable wins
11
+ * over both.
12
+ */
13
+ platform: Platform | 'auto';
6
14
  /** ms, programmatic push/pop */
7
15
  duration: number;
8
16
  /**
9
17
  * the curve a programmatic push/pop runs on. CSS runs it, so it has to be
10
- * one CSS can spell: everything `cubicBezier()` and `parseEasing()` build
11
- * carries a `css` property. A bare `(t) => number` of your own has none, so
12
- * the pages would run `linear` while `progress` reported your curve; give it
13
- * a `css` property, or write the whole transition yourself.
18
+ * one CSS can spell: everything `cubicBezier()`, `bezierPath()` and
19
+ * `parseEasing()` build carries a `css` property. A bare `(t) => number` of
20
+ * your own has none, so the pages would run `linear` while `progress`
21
+ * reported your curve; give it a `css` property, or write the whole
22
+ * transition yourself.
14
23
  */
15
24
  ease: Easing;
25
+ /** fraction of the width the upper page travels (1 = from off-screen; Android slides a short way and fades) */
26
+ travel: number;
16
27
  /** fraction of the width the lower page travels */
17
28
  parallax: number;
29
+ /** opacity of the upper page when fully closed (1 = no fade) */
30
+ fade: number;
18
31
  dimColor: string;
19
32
  /** lower-page overlay opacity at p = 1 (≈0.35 suits dark UIs) */
20
33
  dimMax: number;
@@ -31,11 +44,33 @@ export interface IOSTransitionOptions {
31
44
  timeScale: number;
32
45
  }
33
46
 
47
+ /** Every option except the platform, which is decided once and has no CSS variable. */
48
+ export type NativeTransitionPreset = Readonly<Omit<NativeTransitionOptions, 'platform'>>;
49
+
50
+ /**
51
+ * Each platform's own push/pop, as its system animates it.
52
+ *
53
+ * - `ios`: UIKit's navigation push. The upper page slides the full width with
54
+ * a shadow on its leading edge, the lower page parallaxes 30% and dims.
55
+ * - `android`: the framework's activity open/close since Android 13
56
+ * (`activity_open_enter.xml` and friends): both pages slide 96 dp, about a
57
+ * quarter of a phone, over 450 ms on `fast_out_extra_slow_in`, and the
58
+ * upper page fades through the first part of it. No shadow, no dim.
59
+ */
60
+ export function nativeTransitionPreset(platform: Platform): NativeTransitionPreset {
61
+ const shared = { settleMin: 120, settleMax: 400, settleVelocityFloor: 900, timeScale: 1, dimColor: '#000' };
62
+ return platform === 'android'
63
+ ? { ...shared, duration: 450, ease: easings.android, travel: 0.25, parallax: 0.25, fade: 0, dimMax: 0, shadow: 'none', settleEase: easings.androidSettle }
64
+ : { ...shared, duration: 500, ease: easings.ios, travel: 1, parallax: 0.3, fade: 1, dimMax: 0.1, shadow: '-3px 0 14px rgba(0,0,0,0.16)', settleEase: easings.easeOut };
65
+ }
66
+
34
67
  /** The CSS custom property behind each option. */
35
- export const IOS_TRANSITION_CSS_VARS: Readonly<Record<keyof IOSTransitionOptions, string>> = /*#__PURE__*/ Object.freeze({
68
+ export const NATIVE_TRANSITION_CSS_VARS: Readonly<Record<keyof NativeTransitionPreset, string>> = /*#__PURE__*/ Object.freeze({
36
69
  duration: '--sn-duration',
37
70
  ease: '--sn-easing',
71
+ travel: '--sn-travel',
38
72
  parallax: '--sn-parallax',
73
+ fade: '--sn-fade',
39
74
  dimColor: '--sn-dim-color',
40
75
  dimMax: '--sn-dim-max',
41
76
  shadow: '--sn-shadow',
@@ -46,11 +81,11 @@ export const IOS_TRANSITION_CSS_VARS: Readonly<Record<keyof IOSTransitionOptions
46
81
  timeScale: '--sn-time-scale',
47
82
  });
48
83
 
49
- export interface IOSTransition extends Transition {
50
- /** The JS options: the defaults with the caller's merged in. Mutable at runtime. */
51
- readonly options: IOSTransitionOptions;
84
+ export interface NativeTransition extends Transition {
85
+ /** The JS options: the platform's preset with the caller's merged in. `platform` is the one chosen. Mutable at runtime. */
86
+ readonly options: NativeTransitionOptions & { platform: Platform };
52
87
  /** The values currently in force: `options` with the CSS variables applied over them. */
53
- readonly resolved: Readonly<IOSTransitionOptions>;
88
+ readonly resolved: Readonly<NativeTransitionOptions & { platform: Platform }>;
54
89
  /**
55
90
  * Re-reads the CSS variables, from `el` or from the container of the last
56
91
  * transition. Called at the start of every transition. Call it directly
@@ -60,47 +95,40 @@ export interface IOSTransition extends Transition {
60
95
  }
61
96
 
62
97
  /**
63
- * The iOS navigation transition: the upper page slides in from the trailing
64
- * edge with a shadow on its leading edge, while the lower page parallaxes
65
- * toward the leading edge and dims. Every value is a function of one number, p.
98
+ * The platform's navigation transition: the upper page slides in from the
99
+ * trailing edge while the lower page parallaxes toward the leading edge. On
100
+ * iOS the upper page travels the full width under a shadow and the lower page
101
+ * dims; on Android both travel a short way and the upper page fades. Every
102
+ * value is a function of one number, p.
66
103
  *
67
104
  * Every option is also a CSS custom property on the container (see
68
- * `IOS_TRANSITION_CSS_VARS`), read when a transition starts. A variable that
69
- * is set wins over the JS option, so a stylesheet can slow the animation down
70
- * or restyle it per theme without the app rebuilding the transition.
105
+ * `NATIVE_TRANSITION_CSS_VARS`), read when a transition starts. A variable
106
+ * that is set wins over the JS option, so a stylesheet can slow the animation
107
+ * down or restyle it per theme without the app rebuilding the transition.
71
108
  *
72
109
  * p is only ever written at the ends of a phase. The stack puts that phase's
73
110
  * duration and curve in `--sn-t` / `--sn-e` and CSS interpolates between the
74
111
  * two writes, so the animation costs a handful of style writes rather than one
75
112
  * per page per frame, and runs on the compositor rather than the main thread.
76
113
  */
77
- export function createIOSTransition(options: Partial<IOSTransitionOptions> = {}): IOSTransition {
78
- const o: IOSTransitionOptions = {
79
- duration: 500,
80
- ease: easings.ios,
81
- parallax: 0.3,
82
- dimColor: '#000',
83
- dimMax: 0.1,
84
- shadow: '-3px 0 14px rgba(0,0,0,0.16)',
85
- settleMin: 120,
86
- settleMax: 400,
87
- settleEase: easings.easeOut,
88
- settleVelocityFloor: 900,
89
- timeScale: 1,
90
- ...options,
91
- };
114
+ export function createNativeTransition(options: Partial<NativeTransitionOptions> = {}): NativeTransition {
115
+ const platform = !options.platform || options.platform === 'auto' ? detectPlatform() : options.platform;
116
+ const o: NativeTransitionOptions & { platform: Platform } = { ...nativeTransitionPreset(platform), ...options, platform };
92
117
 
93
118
  let root: Element | null = null;
94
- let r: IOSTransitionOptions = { ...o };
119
+ let r: NativeTransitionOptions & { platform: Platform } = { ...o };
95
120
 
96
121
  const refresh = (el?: Element | null): void => {
97
122
  if (el !== undefined) root = el;
98
123
  const read = cssVars(root);
99
- const v = IOS_TRANSITION_CSS_VARS;
124
+ const v = NATIVE_TRANSITION_CSS_VARS;
100
125
  r = {
126
+ platform: o.platform,
101
127
  duration: parseTime(read(v.duration)) ?? o.duration,
102
128
  ease: parseEasing(read(v.ease)) ?? o.ease,
129
+ travel: parseRatio(read(v.travel)) ?? o.travel,
103
130
  parallax: parseRatio(read(v.parallax)) ?? o.parallax,
131
+ fade: parseRatio(read(v.fade)) ?? o.fade,
104
132
  dimColor: read(v.dimColor) ?? o.dimColor,
105
133
  dimMax: parseRatio(read(v.dimMax)) ?? o.dimMax,
106
134
  shadow: read(v.shadow) ?? o.shadow,
@@ -134,7 +162,7 @@ export function createIOSTransition(options: Partial<IOSTransitionOptions> = {})
134
162
 
135
163
  return {
136
164
  options: o,
137
- get resolved(): Readonly<IOSTransitionOptions> {
165
+ get resolved(): Readonly<NativeTransitionOptions & { platform: Platform }> {
138
166
  return r;
139
167
  },
140
168
  refresh,
@@ -164,7 +192,10 @@ export function createIOSTransition(options: Partial<IOSTransitionOptions> = {})
164
192
  * page, so a resize mid-transition stays honest.
165
193
  */
166
194
  apply(lower, upper, p) {
167
- upper.el.style.transform = shift(1 - p);
195
+ upper.el.style.transform = shift((1 - p) * r.travel);
196
+ // Only a look that fades writes opacity: a property that is not written
197
+ // starts no transition, and a page's own opacity is left alone.
198
+ if (r.fade < 1) upper.el.style.opacity = String(round(r.fade + (1 - r.fade) * p));
168
199
  if (lower) {
169
200
  lower.el.style.transform = shift(-p * r.parallax);
170
201
  dim!.style.opacity = String(p * r.dimMax);
@@ -173,6 +204,7 @@ export function createIOSTransition(options: Partial<IOSTransitionOptions> = {})
173
204
  end(lower, upper) {
174
205
  upper.el.style.boxShadow = '';
175
206
  upper.el.style.transform = '';
207
+ upper.el.style.opacity = '';
176
208
  if (lower) lower.el.style.transform = '';
177
209
  dim?.remove();
178
210
  },
@@ -92,7 +92,9 @@ export class NavigationStack {
92
92
  readonly pageClass: string;
93
93
  entries: StackEntry[] = [];
94
94
  busy = false;
95
- private _queue: Array<() => void> = [];
95
+ private _destroyed = false;
96
+ private _activeTransition: { lower: StackEntry | null; upper: StackEntry } | null = null;
97
+ private _queue: Array<{ run: () => void; cancel: () => void }> = [];
96
98
  private _listeners = new Map<string, Set<Listener<unknown>>>();
97
99
 
98
100
  constructor({ container, transition, pageClass = 'sn-page' }: NavigationStackOptions) {
@@ -130,6 +132,7 @@ export class NavigationStack {
130
132
  };
131
133
  }
132
134
  private _emit<K extends keyof StackEvents>(event: K, detail: StackEvents[K]): void {
135
+ if (this._destroyed) return;
133
136
  const set = this._listeners.get(event);
134
137
  if (set) set.forEach((fn) => fn(detail));
135
138
  }
@@ -251,13 +254,16 @@ export class NavigationStack {
251
254
  this._begin(lower, upper, 'interactive');
252
255
  return {
253
256
  update: (v) => {
257
+ if (this._destroyed) return;
254
258
  p = Math.min(1, Math.max(0, v));
255
259
  this._apply(lower, upper, p);
256
260
  },
257
261
  finish: async ({ complete, velocity = 0 }) => {
262
+ if (this._destroyed) return;
258
263
  const remainingPx = (complete ? p : 1 - p) * this.width();
259
264
  const { duration, ease } = this.transition.settle({ remainingPx, velocity });
260
265
  await this._animate(lower, upper, p, complete ? 0 : 1, duration, ease);
266
+ if (this._destroyed) return;
261
267
  this._end(lower, upper, 'interactive');
262
268
  if (complete) {
263
269
  this.entries.pop();
@@ -271,7 +277,20 @@ export class NavigationStack {
271
277
  };
272
278
  }
273
279
 
280
+ /** Terminal: queued and subsequent navigation reject with AbortError. */
274
281
  destroy(): void {
282
+ if (this._destroyed) return;
283
+ this._destroyed = true;
284
+ this._listeners.clear();
285
+ for (const task of this._queue.splice(0)) task.cancel();
286
+ this.busy = false;
287
+ const active = this._activeTransition;
288
+ this._activeTransition = null;
289
+ if (active) {
290
+ this.transition.end?.(active.lower, active.upper);
291
+ // During a pop the outgoing page has already left entries.
292
+ if (!this.entries.includes(active.upper)) this._unmount(active.upper);
293
+ }
275
294
  while (this.entries.length) this._unmount(this.entries.pop()!);
276
295
  this.container.classList.remove('sn-container', 'sn-busy');
277
296
  this.container.style.removeProperty('--sn-t');
@@ -280,6 +299,7 @@ export class NavigationStack {
280
299
 
281
300
  // -------------------------------------------------------------- internals
282
301
  private _setBusy(v: boolean): void {
302
+ if (this._destroyed) return;
283
303
  this.busy = v;
284
304
  this.container.classList.toggle('sn-busy', v);
285
305
  }
@@ -287,6 +307,8 @@ export class NavigationStack {
287
307
  /** Serializes operations: while a transition runs, later calls wait their turn. */
288
308
  private _run<R>(fn: () => Promise<R>): Promise<R> {
289
309
  return new Promise<R>((resolve, reject) => {
310
+ const cancel = () => reject(new DOMException('NavigationStack has been destroyed', 'AbortError'));
311
+ if (this._destroyed) return cancel();
290
312
  const task = async () => {
291
313
  this._setBusy(true);
292
314
  try {
@@ -298,12 +320,12 @@ export class NavigationStack {
298
320
  this._drain();
299
321
  }
300
322
  };
301
- if (this.busy) this._queue.push(task);
323
+ if (this.busy) this._queue.push({ run: task, cancel });
302
324
  else task();
303
325
  });
304
326
  }
305
327
  private _drain(): void {
306
- if (!this.busy && this._queue.length) this._queue.shift()!();
328
+ if (!this._destroyed && !this.busy && this._queue.length) this._queue.shift()!.run();
307
329
  }
308
330
 
309
331
  private async _popRevealing(depth: number, animated: boolean, source: NavigationSource): Promise<StackEntry> {
@@ -312,6 +334,7 @@ export class NavigationStack {
312
334
  while (this.entries.length > depth) removed.push(this._unmount(this.entries.pop()!)); // intermediate pages: removed without animation
313
335
  const lower = this.top;
314
336
  await this._transition(lower, upper, 1, 0, animated, 'pop');
337
+ if (this._destroyed) return upper;
315
338
  removed.push(this._unmount(upper));
316
339
  this._settle();
317
340
  this._emit('pop', { entry: upper, removed, entries: this.entries.slice(), source });
@@ -344,11 +367,13 @@ export class NavigationStack {
344
367
  * `prefers-reduced-motion` reduces every phase to.
345
368
  */
346
369
  private _timing(duration: number, ease?: Easing): void {
370
+ if (this._destroyed) return;
347
371
  this.container.style.setProperty('--sn-t', cssDuration(duration));
348
372
  this.container.style.setProperty('--sn-e', cssEasing(ease));
349
373
  }
350
374
 
351
375
  private _begin(lower: StackEntry | null, upper: StackEntry, kind: TransitionKind): void {
376
+ this._activeTransition = { lower, upper };
352
377
  this._timing(0);
353
378
  lower?.el.classList.add('sn-page-visible', 'sn-page-lower');
354
379
  upper.el.classList.add('sn-page-visible', 'sn-page-upper');
@@ -360,6 +385,8 @@ export class NavigationStack {
360
385
  this._emit('progress', { lower, upper, p });
361
386
  }
362
387
  private _end(lower: StackEntry | null, upper: StackEntry, kind: TransitionKind): void {
388
+ if (this._destroyed) return;
389
+ this._activeTransition = null;
363
390
  this._timing(0);
364
391
  upper.el.classList.remove('sn-page-upper');
365
392
  lower?.el.classList.remove('sn-page-lower');
@@ -0,0 +1,23 @@
1
+ // Which native platform the page is running on, so the transition can follow
2
+ // that platform's own push/pop. Nothing here is certain: user agents lie, and
3
+ // desktop browsers are neither. iOS is the default when nothing is recognized,
4
+ // because that is the look most web apps expect from a stack transition.
5
+
6
+ export type Platform = 'ios' | 'android';
7
+
8
+ export function isIOSBrowser(): boolean {
9
+ if (typeof navigator === 'undefined') return false;
10
+ return /iP(hone|ad|od)/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
11
+ }
12
+
13
+ export function isAndroidBrowser(): boolean {
14
+ if (typeof navigator === 'undefined') return false;
15
+ const hints = (navigator as { userAgentData?: { platform?: string } }).userAgentData;
16
+ if (hints?.platform) return hints.platform === 'Android';
17
+ return /\bAndroid\b/.test(navigator.userAgent ?? '');
18
+ }
19
+
20
+ /** `android` on an Android browser, otherwise `ios`. */
21
+ export function detectPlatform(): Platform {
22
+ return !isIOSBrowser() && isAndroidBrowser() ? 'android' : 'ios';
23
+ }
package/src/styles.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The only styles the engine needs. These cover layout and the motion itself,
3
3
  * not appearance: the look lives in the transition and is tuned through the
4
- * `--sn-*` custom properties (see `IOS_TRANSITION_CSS_VARS` and the README).
4
+ * `--sn-*` custom properties (see `NATIVE_TRANSITION_CSS_VARS` and the README).
5
5
  * Those properties are documented but deliberately not declared, because
6
6
  * leaving one unset is what makes its JS default apply.
7
7
  *
@@ -20,8 +20,7 @@
20
20
  * - `.sn-container:dir(rtl)`: reading direction is a CSS question, so the
21
21
  * transform the engine writes is signed by `--sn-dir` rather than by JS.
22
22
  * - `.sn-page`: absolutely fills the container and is its own scroll container.
23
- * `touch-action: pan-y` keeps vertical scrolling native while horizontal
24
- * drags reach the gesture; `visibility: hidden` keeps pages beneath the top
23
+ * `visibility: hidden` keeps pages beneath the top
25
24
  * mounted (scroll position, form state) but out of sight and out of the
26
25
  * accessibility tree. The identity transform is the resting state, and the
27
26
  * containing block the dim overlay is positioned against.
@@ -30,14 +29,11 @@
30
29
  * transition in flight. Only these transition, and only these are promoted,
31
30
  * so a deep stack costs nothing at rest. Both `transform` and `opacity` are
32
31
  * listed, so a transition of your own can fade a page as well as move it and
33
- * still be run by the browser; the iOS look only ever changes `transform`,
34
- * and a property that does not change starts no transition.
32
+ * still be run by the browser; the iOS look only ever changes `transform`
33
+ * (the Android look fades as well), and a property that does not change
34
+ * starts no transition.
35
35
  * - `.sn-dim`: the overlay the lower page dims behind. CSS resolves its colour;
36
36
  * the transition supplies a fallback colour and writes its opacity.
37
- * - `.sn-edge`: the strip the swipe-back gesture listens on. `inset-inline-start`
38
- * puts it on the leading edge in either reading direction; its width is the
39
- * gesture's `edgeWidth` option, and whether it is shown at all follows from
40
- * the container's classes.
41
37
  * - `.sn-busy`: no clicks land on a page that is mid-transition, and a drag
42
38
  * does not select the text under it.
43
39
  * - `prefers-reduced-motion`: forces the phase duration to zero in CSS. The
@@ -47,12 +43,10 @@
47
43
  export const STACKNAV_CSS =
48
44
  '.sn-container{position:relative;overflow:hidden}' +
49
45
  '.sn-container:dir(rtl){--sn-dir:-1}' +
50
- '.sn-page{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;touch-action:pan-y;visibility:hidden;transform:translate3d(0,0,0)}' +
46
+ '.sn-page{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;visibility:hidden;transform:translate3d(0,0,0)}' +
51
47
  '.sn-page-visible{visibility:visible}' +
52
48
  '.sn-page-upper,.sn-page-lower{will-change:transform;transition-property:transform,opacity;transition-duration:var(--sn-t,0s);transition-timing-function:var(--sn-e,linear)}' +
53
49
  '.sn-dim{position:fixed;inset:0;z-index:2147483647;pointer-events:none;background:var(--sn-dim-color,var(--sn-dim-fallback,#000));opacity:0;transition:opacity var(--sn-t,0s) var(--sn-e,linear)}' +
54
- '.sn-edge{position:absolute;inset-block:0;inset-inline-start:0;z-index:10;touch-action:none}' +
55
- '.sn-container:not(.sn-can-pop) .sn-edge,.sn-container.sn-anywhere .sn-edge{display:none}' +
56
50
  '.sn-busy{user-select:none;-webkit-user-select:none}' +
57
51
  '.sn-busy .sn-page{pointer-events:none}' +
58
52
  '@media(prefers-reduced-motion:reduce){.sn-container{--sn-t:0s!important}}';
@@ -0,0 +1,35 @@
1
+ /** Browser suppression is best effort; browser buttons and OS gestures remain available. */
2
+ export type SwipeBackMode = 'browser' | 'disabled';
3
+
4
+ interface Suppression {
5
+ count: number;
6
+ value: string;
7
+ priority: string;
8
+ }
9
+
10
+ // Several outlets can share one viewport. Restore it only after the last
11
+ // suppression request is released, including when stacks are destroyed.
12
+ const suppressions = /*#__PURE__*/ new WeakMap<HTMLElement, Suppression>();
13
+
14
+ export function suppressBrowserSwipe(container: HTMLElement): () => void {
15
+ const root = container.ownerDocument.documentElement;
16
+ const property = 'overscroll-behavior-x';
17
+ let state = suppressions.get(root);
18
+ if (!state) {
19
+ state = { count: 0, value: root.style.getPropertyValue(property), priority: root.style.getPropertyPriority(property) };
20
+ suppressions.set(root, state);
21
+ root.style.setProperty(property, 'contain', 'important');
22
+ }
23
+ state.count++;
24
+ let released = false;
25
+ return () => {
26
+ if (released) return;
27
+ released = true;
28
+ if (--state.count) return;
29
+ suppressions.delete(root);
30
+ // Do not overwrite a newer declaration installed by the application.
31
+ if (root.style.getPropertyValue(property) !== 'contain' || root.style.getPropertyPriority(property) !== 'important') return;
32
+ if (state.value) root.style.setProperty(property, state.value, state.priority);
33
+ else root.style.removeProperty(property);
34
+ };
35
+ }
@@ -1,36 +0,0 @@
1
- import type { NavigationStack } from './navigation-stack.ts';
2
- export interface EdgePanGestureOptions {
3
- /** px strip on the leading edge that starts the gesture */
4
- edgeWidth: number;
5
- /** recognize the drag from anywhere on the page */
6
- anywhere: boolean;
7
- /** px of horizontal movement before the drag begins */
8
- startSlop: number;
9
- /** px of vertical movement that hands the touch back to scrolling */
10
- verticalCancelSlop: number;
11
- /** fraction of the width dragged that completes without velocity */
12
- completeThreshold: number;
13
- /** px/s toward the trailing edge that completes the pop regardless of distance */
14
- completeVelocity: number;
15
- /** px/s back toward the leading edge that cancels the pop regardless of distance */
16
- cancelVelocity: number;
17
- velocitySamples: number;
18
- }
19
- export interface EdgePanGesture {
20
- readonly options: EdgePanGestureOptions;
21
- /** Re-reads options changed at runtime, such as `edgeWidth` and `anywhere`. */
22
- refresh(): void;
23
- attach(stack: NavigationStack): EdgePanGesture;
24
- detach(): void;
25
- }
26
- /**
27
- * Recognizes a horizontal drag from the leading edge and drives the stack's
28
- * interactive pop from it. Built on pointer events, so it handles both mouse
29
- * and touch. Vertical movement early in the gesture hands the touch back to
30
- * native scrolling.
31
- *
32
- * "Leading" is whichever edge the container reads from, so in a right-to-left
33
- * container the strip sits on the right and back is a drag to the left.
34
- */
35
- export declare function createEdgePanGesture(options?: Partial<EdgePanGestureOptions>): EdgePanGesture;
36
- //# sourceMappingURL=edge-pan-gesture.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"edge-pan-gesture.d.ts","sourceRoot":"","sources":["../src/edge-pan-gesture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEnF,MAAM,WAAW,qBAAqB;IACpC,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,QAAQ,EAAE,OAAO,CAAC;IAClB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,+EAA+E;IAC/E,OAAO,IAAI,IAAI,CAAC;IAChB,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,cAAc,CAAC;IAC/C,MAAM,IAAI,IAAI,CAAC;CAChB;AAcD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,OAAO,CAAC,qBAAqB,CAAM,GAAG,cAAc,CA4IjG"}
@@ -1,155 +0,0 @@
1
- /**
2
- * Recognizes a horizontal drag from the leading edge and drives the stack's
3
- * interactive pop from it. Built on pointer events, so it handles both mouse
4
- * and touch. Vertical movement early in the gesture hands the touch back to
5
- * native scrolling.
6
- *
7
- * "Leading" is whichever edge the container reads from, so in a right-to-left
8
- * container the strip sits on the right and back is a drag to the left.
9
- */
10
- export function createEdgePanGesture(options = {}) {
11
- const o = {
12
- edgeWidth: 28,
13
- anywhere: false,
14
- startSlop: 6,
15
- verticalCancelSlop: 10,
16
- completeThreshold: 0.5,
17
- completeVelocity: 500,
18
- cancelVelocity: -500,
19
- velocitySamples: 6,
20
- ...options,
21
- };
22
- let stack;
23
- let strip = null;
24
- /**
25
- * Which way forward is, read the same way the pages read it: `--sn-dir` when
26
- * the stylesheet sets it, the container's reading direction otherwise. Taking
27
- * it from one place is what keeps the drag and the transition from disagreeing
28
- * about which edge is the back edge.
29
- */
30
- const direction = () => {
31
- const style = typeof getComputedStyle === 'function' ? getComputedStyle(stack.container) : null;
32
- const declared = Number(style?.getPropertyValue('--sn-dir'));
33
- if (declared < 0)
34
- return -1;
35
- if (declared > 0)
36
- return 1;
37
- return style?.direction === 'rtl' ? -1 : 1;
38
- };
39
- let drag = null;
40
- let suppressClick = false;
41
- let offs = [];
42
- const onDown = (ev) => {
43
- if (drag || !stack.canPop())
44
- return;
45
- if (ev.pointerType === 'mouse' && ev.button !== 0)
46
- return;
47
- if (!o.anywhere && ev.target !== strip)
48
- return;
49
- drag = { id: ev.pointerId, target: ev.currentTarget, x0: ev.clientX, y0: ev.clientY, dir: direction(), handle: null, p: 1, samples: [[ev.clientX, performance.now()]] };
50
- };
51
- const onMove = (ev) => {
52
- if (!drag || ev.pointerId !== drag.id)
53
- return;
54
- // Signed so that "forward along the drag" is always positive, whichever
55
- // edge the container calls leading.
56
- const dx = (ev.clientX - drag.x0) * drag.dir;
57
- const dy = ev.clientY - drag.y0;
58
- if (!drag.handle) {
59
- if (Math.abs(dy) > o.verticalCancelSlop && Math.abs(dy) > Math.abs(dx)) {
60
- drag = null;
61
- return;
62
- }
63
- if (dx < o.startSlop)
64
- return;
65
- const handle = stack.beginInteractivePop();
66
- if (!handle) {
67
- drag = null;
68
- return;
69
- }
70
- drag.handle = handle;
71
- try {
72
- drag.target.setPointerCapture?.(ev.pointerId);
73
- }
74
- catch {
75
- /* capture is best-effort */
76
- }
77
- }
78
- drag.samples.push([ev.clientX, performance.now()]);
79
- if (drag.samples.length > o.velocitySamples)
80
- drag.samples.shift();
81
- drag.p = 1 - Math.min(1, Math.max(0, (dx - o.startSlop) / stack.width()));
82
- drag.handle.update(drag.p);
83
- };
84
- const onUp = (ev) => {
85
- if (!drag || ev.pointerId !== drag.id)
86
- return;
87
- const d = drag;
88
- drag = null;
89
- if (!d.handle)
90
- return;
91
- const s = d.samples;
92
- const [x1, t1] = s[0];
93
- const [x2, t2] = s[s.length - 1];
94
- const velocity = (t2 > t1 ? ((x2 - x1) / (t2 - t1)) * 1000 : 0) * d.dir;
95
- const cancelled = ev.type === 'pointercancel';
96
- const complete = !cancelled && (velocity > o.completeVelocity || (d.p < 1 - o.completeThreshold && velocity > o.cancelVelocity));
97
- // The click that follows a drag release must not activate whatever is under the pointer.
98
- suppressClick = true;
99
- setTimeout(() => {
100
- suppressClick = false;
101
- }, 0);
102
- void d.handle.finish({ complete, velocity });
103
- };
104
- const onClick = (ev) => {
105
- if (suppressClick) {
106
- ev.stopPropagation();
107
- ev.preventDefault();
108
- }
109
- };
110
- const EVENTS = { pointerdown: onDown, pointermove: onMove, pointerup: onUp, pointercancel: onUp };
111
- const listen = (el) => Object.entries(EVENTS).forEach(([k, f]) => el.addEventListener(k, f));
112
- const unlisten = (el) => Object.entries(EVENTS).forEach(([k, f]) => el.removeEventListener(k, f));
113
- /**
114
- * Whether the strip is shown at all is a CSS question — the stylesheet hides
115
- * it when there is nothing to go back to, or when the whole page is the
116
- * target — so this only has to say what is true.
117
- */
118
- const refresh = () => {
119
- if (!strip)
120
- return;
121
- strip.style.width = o.edgeWidth + 'px';
122
- stack.container.classList.toggle('sn-anywhere', o.anywhere);
123
- stack.container.classList.toggle('sn-can-pop', stack.entries.length > 1);
124
- };
125
- const gesture = {
126
- options: o,
127
- refresh,
128
- attach(s) {
129
- stack = s;
130
- strip = document.createElement('div');
131
- strip.className = 'sn-edge';
132
- strip.setAttribute('aria-hidden', 'true');
133
- stack.container.append(strip);
134
- // Pointer events from the strip bubble here; one listener per event
135
- // avoids applying each move and recording its velocity sample twice.
136
- listen(stack.container);
137
- stack.container.addEventListener('click', onClick, true);
138
- offs = ['push', 'pop', 'replace', 'reset'].map((e) => stack.on(e, refresh));
139
- refresh();
140
- return gesture;
141
- },
142
- detach() {
143
- if (!strip)
144
- return;
145
- offs.forEach((f) => f());
146
- unlisten(stack.container);
147
- stack.container.removeEventListener('click', onClick, true);
148
- stack.container.classList.remove('sn-anywhere', 'sn-can-pop');
149
- strip.remove();
150
- strip = null;
151
- },
152
- };
153
- return gesture;
154
- }
155
- //# sourceMappingURL=edge-pan-gesture.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"edge-pan-gesture.js","sourceRoot":"","sources":["../src/edge-pan-gesture.ts"],"names":[],"mappings":"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"}