@stacknav/core 0.2.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 (48) hide show
  1. package/README.md +235 -0
  2. package/dist/animate.d.ts +26 -0
  3. package/dist/animate.d.ts.map +1 -0
  4. package/dist/animate.js +69 -0
  5. package/dist/animate.js.map +1 -0
  6. package/dist/css-vars.d.ts +22 -0
  7. package/dist/css-vars.d.ts.map +1 -0
  8. package/dist/css-vars.js +89 -0
  9. package/dist/css-vars.js.map +1 -0
  10. package/dist/direction.d.ts +79 -0
  11. package/dist/direction.d.ts.map +1 -0
  12. package/dist/direction.js +98 -0
  13. package/dist/direction.js.map +1 -0
  14. package/dist/edge-pan-gesture.d.ts +33 -0
  15. package/dist/edge-pan-gesture.d.ts.map +1 -0
  16. package/dist/edge-pan-gesture.js +128 -0
  17. package/dist/edge-pan-gesture.js.map +1 -0
  18. package/dist/history-adapter.d.ts +17 -0
  19. package/dist/history-adapter.d.ts.map +1 -0
  20. package/dist/history-adapter.js +44 -0
  21. package/dist/history-adapter.js.map +1 -0
  22. package/dist/index.d.ts +34 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +30 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ios-transition.d.ts +50 -0
  27. package/dist/ios-transition.d.ts.map +1 -0
  28. package/dist/ios-transition.js +122 -0
  29. package/dist/ios-transition.js.map +1 -0
  30. package/dist/navigation-stack.d.ts +177 -0
  31. package/dist/navigation-stack.d.ts.map +1 -0
  32. package/dist/navigation-stack.js +296 -0
  33. package/dist/navigation-stack.js.map +1 -0
  34. package/dist/stacknav.css +33 -0
  35. package/dist/styles.d.ts +29 -0
  36. package/dist/styles.d.ts.map +1 -0
  37. package/dist/styles.js +39 -0
  38. package/dist/styles.js.map +1 -0
  39. package/package.json +43 -0
  40. package/src/animate.ts +80 -0
  41. package/src/css-vars.ts +91 -0
  42. package/src/direction.ts +146 -0
  43. package/src/edge-pan-gesture.ts +162 -0
  44. package/src/history-adapter.ts +52 -0
  45. package/src/index.ts +86 -0
  46. package/src/ios-transition.ts +163 -0
  47. package/src/navigation-stack.ts +362 -0
  48. package/src/styles.ts +40 -0
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # @stacknav/core
2
+
3
+ An iOS push/pop navigation transition for any web app. Pages, headers and
4
+ styling stay the app's own; the engine only moves them.
5
+
6
+ - **Push / pop** with the UIKit curve, parallax and dim on the page beneath, and
7
+ a shadow on the leading edge of the incoming page.
8
+ - **Interactive pop**: drag from the leading edge (or from anywhere, if
9
+ configured) and the page follows the pointer. Release past half the width or
10
+ with a flick to complete; a flick back cancels.
11
+ - **Scroll and state are preserved.** Pages beneath the top stay mounted and
12
+ hidden. Only `transform` is written, so scroll offsets, form state and focus
13
+ survive.
14
+ - **Direction resolution** for router-driven apps: composable strategies decide
15
+ push / pop / replace.
16
+ - **Browser back** for apps without a router, through an optional history
17
+ adapter. On iOS browsers the pop is instant, because Safari has already
18
+ animated its own snapshot.
19
+ - **Tunable from CSS.** Duration, curve, parallax, dim and shadow are custom
20
+ properties on the container, so a media query or theme class can retune the
21
+ animation without touching the app's JS.
22
+ - **Custom chrome.** Subscribe to `progress` events and drive a fixed header or
23
+ tab bar from the same `p`.
24
+ - No dependencies. Plain ES modules with type declarations.
25
+
26
+ ## Use
27
+
28
+ ```html
29
+ <div id="app"></div>
30
+ <script type="module">
31
+ import { createIOSStack, attachBrowserHistory, injectStyles } from '@stacknav/core';
32
+
33
+ injectStyles(); // or <link rel="stylesheet" href="@stacknav/core/stacknav.css">
34
+ const nav = createIOSStack({ container: document.getElementById('app') });
35
+ await nav.push(homePage(), { animated: false });
36
+ attachBrowserHistory(nav);
37
+
38
+ nav.push(detailPage(item)); // resolves when the animation ends
39
+ nav.pop();
40
+ nav.popTo(1); // back to the root
41
+ </script>
42
+ ```
43
+
44
+ The container needs a height; it becomes `position: relative; overflow: hidden`.
45
+ Each page is an element you create. The stack gives it
46
+ `position: absolute; inset: 0; overflow-y: auto` and manages its visibility.
47
+
48
+ ## API
49
+
50
+ ### `createIOSStack({ container, transition?, gesture? })`
51
+
52
+ Builds a `NavigationStack` with the iOS transition and the edge-pan gesture
53
+ attached. `transition` and `gesture` are option objects for the two factories
54
+ below. The gesture is exposed as `stack.gesture`; `stack.destroy()` detaches it.
55
+
56
+ ### `NavigationStack`
57
+
58
+ | Member | Description |
59
+ | --- | --- |
60
+ | `push(el \| () => el, { animated, data, key, source })` | Mounts and slides in a page. Resolves with the entry `{ el, index, key, data }`. An element already lower in the stack is moved to the top. |
61
+ | `pop({ animated })` | Slides the top page out. Resolves with the removed entry, or `null` at the root. |
62
+ | `popTo(depth, { animated })` | Pops until `depth` pages remain. Intermediate pages are removed without animation. |
63
+ | `popWith(el, opts)` | Pops the top page, revealing `el`. If `el` is mounted beneath, everything above it is removed; if not, it is placed beneath the top first. Used by routers to pop to a page that no longer exists. |
64
+ | `replace(el, opts)` | Swaps the top page for `el`, no animation. |
65
+ | `present(el, direction, opts)` | `push`, `pop` (via `popWith`) or `replace`, for callers that already resolved the direction. |
66
+ | `remove(el)` | Drops a page wherever it sits, no animation. |
67
+ | `reset(elements)` | Replaces the whole stack, no animation. |
68
+ | `beginInteractivePop()` | Returns `{ update(p), finish({ complete, velocity }) }` or `null`. Used by the gesture, and usable by a custom recognizer. |
69
+ | `depth`, `top`, `entries`, `busy`, `canPop()`, `entryOf(el \| key)` | State. |
70
+ | `on(event, fn)` | Events: `push`, `pop`, `replace`, `reset`, `transitionstart`, `progress`, `transitionend`. Returns an unsubscribe function. |
71
+ | `destroy()` | Unmounts everything. |
72
+
73
+ Operations are serialized: a `push` called during a transition waits its turn.
74
+
75
+ The `pop` event carries `{ entry, removed, entries, source }`, where `source` is
76
+ `"api"`, `"gesture"`, `"history"` or whatever a caller passed. Removed elements
77
+ are detached, not destroyed.
78
+
79
+ ### Direction resolution
80
+
81
+ ```ts
82
+ import { createDirectionResolver, fromHint, fromHistory, fromStack, fromLevel, fromTree, always } from '@stacknav/core';
83
+
84
+ const resolve = createDirectionResolver([fromHint(), fromHistory(), fromStack(), fromLevel(), fromTree()], 'push');
85
+
86
+ resolve({
87
+ from: { key: '/items', segments: ['items'] },
88
+ to: { key: '/items/42', segments: ['items', '42'], level: 2 },
89
+ trigger: 'imperative', // or 'history'
90
+ historyDelta: undefined, // negative = back, positive = forward, when known
91
+ hint: undefined, // an explicit 'push' | 'pop' | 'replace'
92
+ stack: ['/', '/items'], // keys of the kept pages, bottom to top
93
+ }); // → 'push'
94
+ ```
95
+
96
+ A strategy is
97
+ `(ctx: NavigationContext) => 'push' | 'pop' | 'replace' | 'auto' | undefined`.
98
+ `fromLevel({ sameLevel })` and `fromTree({ sameDepth })` configure what equal
99
+ numbers or unrelated siblings mean (default `replace`). `segmentsOf(url)` splits
100
+ a path into `segments`.
101
+
102
+ ### `createIOSTransition(options)`
103
+
104
+ | Option | CSS variable | Default | Description |
105
+ | --- | --- | --- | --- |
106
+ | `duration` | `--sn-duration` | `500` | ms for a programmatic push/pop |
107
+ | `ease` | `--sn-easing` | `cubic-bezier(0.32, 0.72, 0, 1)` | the curve push/pop runs on |
108
+ | `parallax` | `--sn-parallax` | `0.3` | fraction of the width the lower page travels |
109
+ | `dimColor`, `dimMax` | `--sn-dim-color`, `--sn-dim-max` | `"#000"`, `0.1` | overlay on the lower page at full open (`0.35` suits dark UIs) |
110
+ | `shadow` | `--sn-shadow` | `-3px 0 14px rgba(0,0,0,0.16)` | box-shadow on the incoming page |
111
+ | `settleMin`, `settleMax` | `--sn-settle-min`, `--sn-settle-max` | `120`, `400` | ms bounds when finishing an interactive pop |
112
+ | `settleEase` | `--sn-settle-easing` | `cubic-bezier(0.2, 0.8, 0.2, 1)` | the curve a released swipe finishes on |
113
+ | `settleVelocityFloor` | `--sn-settle-velocity-floor` | `900` | px/s assumed when the pointer was slower |
114
+ | `timeScale` | `--sn-time-scale` | `1` | multiplies every duration (slow motion, tests) |
115
+
116
+ `prefers-reduced-motion` sets every duration to 0.
117
+
118
+ #### Tuning from CSS
119
+
120
+ Every option is also a custom property, read off the container when a transition
121
+ starts. Custom properties inherit, so set them wherever you like: on `:root`, on
122
+ the container, under a theme class, or inside a media query.
123
+
124
+ ```css
125
+ :root {
126
+ --sn-duration: 340ms; /* snappier than iOS */
127
+ --sn-easing: cubic-bezier(0.4, 0, 0.2, 1);
128
+ }
129
+ .theme-flat {
130
+ --sn-parallax: 0; /* no parallax on the page beneath */
131
+ --sn-dim-max: 0;
132
+ --sn-shadow: none;
133
+ }
134
+ @media (prefers-color-scheme: dark) {
135
+ :root { --sn-dim-max: 35%; } /* stronger dimming on dark UIs */
136
+ }
137
+ ```
138
+
139
+ Durations accept `ms`, `s` or a bare number of milliseconds. Fractions accept
140
+ `0.3` or `30%`. Easings accept `linear`, `ease`, `ease-in`, `ease-out`,
141
+ `ease-in-out`, `cubic-bezier(…)` with x coordinates within `[0, 1]` as CSS
142
+ requires, or `ios` / `ios-settle` for the two defaults. The `step` and `linear()`
143
+ timing functions are not supported.
144
+
145
+ Precedence:
146
+
147
+ 1. A variable that is set wins over the JS option, so a stylesheet can retune a
148
+ transition the app configured in code.
149
+ 2. A variable that is unset falls through to the JS option, so nothing needs to
150
+ be declared to get the defaults.
151
+ 3. A variable the engine cannot parse also falls through to the JS option. A bad
152
+ value degrades to the default rather than breaking the animation, and never
153
+ reaches the tween. This includes `calc()` and other math: custom properties
154
+ reach `getComputedStyle` unevaluated, so `--sn-duration: calc(var(--speed) *
155
+ 2)` cannot be read and falls back. Do the arithmetic where you define the
156
+ variable.
157
+
158
+ Values are re-read at the start of every transition, which covers media queries
159
+ and class changes. `transition.refresh()` re-reads them on demand, for example
160
+ after changing `transition.options` mid-animation. `transition.resolved` is what
161
+ is currently in force, and `IOS_TRANSITION_CSS_VARS` maps each option to its
162
+ variable name.
163
+
164
+ A transition is just
165
+ `{ duration, ease, settle(), begin?(), apply(lower, upper, p), end?() }`, so you
166
+ can write a different one (a fade, a vertical sheet) and pass it to
167
+ `new NavigationStack({ container, transition })`. `cssVars()` and the
168
+ `parseTime` / `parseNumber` / `parseRatio` / `parseEasing` helpers are exported
169
+ so a custom transition can read variables the same way. Each returns `undefined`
170
+ rather than `NaN` for anything it cannot parse, so `?? yourDefault` is all the
171
+ handling a value needs.
172
+
173
+ ### `createEdgePanGesture(options)`
174
+
175
+ | Option | Default | Description |
176
+ | --- | --- | --- |
177
+ | `edgeWidth` | `28` | px strip on the leading edge that starts the gesture |
178
+ | `anywhere` | `false` | recognize the drag from anywhere on the page |
179
+ | `startSlop` | `6` | px of horizontal travel before the drag begins |
180
+ | `verticalCancelSlop` | `10` | px of vertical travel that hands the touch to scrolling |
181
+ | `completeThreshold` | `0.5` | fraction of the width that completes on a slow release |
182
+ | `completeVelocity` | `500` | px/s toward the trailing edge that completes regardless |
183
+ | `cancelVelocity` | `-500` | px/s back toward the leading edge that cancels regardless |
184
+
185
+ Call `gesture.refresh()` after changing options at runtime.
186
+
187
+ ### `attachBrowserHistory(stack, { key = "snDepth", animateHistoryPop, onForward })`
188
+
189
+ For apps without a router. Mirrors stack depth into `history.state`. Returns a
190
+ detach function. `animateHistoryPop` defaults to `false` on iOS browsers and
191
+ `true` elsewhere. Forward navigation has no page to show, so by default it
192
+ bounces back; pass `onForward(targetDepth)` to re-push something instead.
193
+
194
+ ### Styles
195
+
196
+ `injectStyles()` inserts the engine's four rules once. `STACKNAV_CSS` is the same
197
+ CSS as a string, minified because it rides along in your JS bundle, and
198
+ `@stacknav/core/stacknav.css` is the same CSS as a readable file. It covers
199
+ layout only and declares no custom properties: the tuning variables above are
200
+ listed in a comment in the file rather than set, so that leaving one out means
201
+ "use the default".
202
+
203
+ ## Footprint
204
+
205
+ Plain ES modules, no dependencies, no work at module load: a bundler keeps only what you import, whether or not it honours the package's `sideEffects` flag (the tests bundle each entry point with that flag switched off and check what survives). Minified and gzipped, as measured by `pnpm size`:
206
+
207
+ | You import | Costs |
208
+ | --- | --- |
209
+ | `createIOSStack` (stack, iOS look, swipe back) | ~4.1 kB |
210
+ | `NavigationStack` with your own transition | ~2.0 kB |
211
+ | the direction strategies | ~0.6 kB |
212
+ | `attachBrowserHistory` | ~0.4 kB |
213
+ | `injectStyles` | ~0.3 kB |
214
+ | everything | ~5.4 kB |
215
+
216
+ ## Develop
217
+
218
+ ```sh
219
+ pnpm test # node:test with a 68-line DOM stub, no browser; includes the tree-shaking checks
220
+ pnpm build # tsc → dist/, plus dist/stacknav.css
221
+ pnpm size # what each entry point costs, minified + gzipped (after a build)
222
+ ```
223
+
224
+ ```
225
+ src/
226
+ animate.ts cubic-bezier solver, cancellable tween, easings
227
+ css-vars.ts reading and parsing the engine's custom properties
228
+ navigation-stack.ts the stack: mounting, ordering, transition lifecycle, queueing
229
+ ios-transition.ts the look: slide, parallax, dim, shadow, settle timing
230
+ edge-pan-gesture.ts pointer-event recognizer that drives the interactive pop
231
+ direction.ts push / pop / replace strategies and the resolver
232
+ history-adapter.ts history.state mirroring for apps without a router
233
+ styles.ts the CSS the engine needs, and injectStyles()
234
+ index.ts exports + createIOSStack()
235
+ ```
@@ -0,0 +1,26 @@
1
+ export type Easing = (t: number) => number;
2
+ export declare function cubicBezier(x1: number, y1: number, x2: number, y2: number): Easing;
3
+ export declare const easings: {
4
+ linear: Easing;
5
+ ios: Easing;
6
+ easeOut: Easing;
7
+ };
8
+ export interface TweenOptions {
9
+ from: number;
10
+ to: number;
11
+ duration: number;
12
+ ease?: Easing;
13
+ onUpdate: (value: number) => void;
14
+ }
15
+ export type CancellableTween = Promise<void> & {
16
+ cancel(): void;
17
+ };
18
+ /**
19
+ * Animates a number from `from` to `to` over `duration` ms, calling `onUpdate`
20
+ * every frame. Returns a promise that resolves when the tween is done;
21
+ * `promise.cancel()` stops it early. A duration of 0 or less jumps straight
22
+ * to `to`.
23
+ */
24
+ export declare function tween({ from, to, duration, ease, onUpdate }: TweenOptions): CancellableTween;
25
+ export declare const prefersReducedMotion: () => boolean;
26
+ //# sourceMappingURL=animate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate.d.ts","sourceRoot":"","sources":["../src/animate.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3C,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAiBlF;AAKD,eAAO,MAAM,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAInE,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG;IAAE,MAAM,IAAI,IAAI,CAAA;CAAE,CAAC;AAElE;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAqB,EAAE,QAAQ,EAAE,EAAE,YAAY,GAAG,gBAAgB,CA2B7G;AAED,eAAO,MAAM,oBAAoB,QAAO,OACoD,CAAC"}
@@ -0,0 +1,69 @@
1
+ // A small animation toolkit: a cubic-bezier solver, a cancellable tween, and
2
+ // the easing curves the iOS transition uses.
3
+ export function cubicBezier(x1, y1, x2, y2) {
4
+ const A = (a, b) => 1 - 3 * b + 3 * a;
5
+ const B = (a, b) => 3 * b - 6 * a;
6
+ const C = (a) => 3 * a;
7
+ const calc = (t, a, b) => ((A(a, b) * t + B(a, b)) * t + C(a)) * t;
8
+ const slope = (t, a, b) => 3 * A(a, b) * t * t + 2 * B(a, b) * t + C(a);
9
+ return (x) => {
10
+ if (x <= 0)
11
+ return 0;
12
+ if (x >= 1)
13
+ return 1;
14
+ let t = x;
15
+ for (let i = 0; i < 8; i++) {
16
+ const s = slope(t, x1, x2);
17
+ if (s === 0)
18
+ break;
19
+ t -= (calc(t, x1, x2) - x) / s;
20
+ }
21
+ return calc(t, y1, y2);
22
+ };
23
+ }
24
+ // `#__PURE__` marks the module-load calls as droppable, so a bundler that does
25
+ // not honour the package's `sideEffects` flag can still leave this module out
26
+ // when nothing here is imported.
27
+ export const easings = {
28
+ linear: (t) => t,
29
+ ios: /*#__PURE__*/ cubicBezier(0.32, 0.72, 0, 1), // the common approximation of UIKit's navigation curve
30
+ easeOut: /*#__PURE__*/ cubicBezier(0.2, 0.8, 0.2, 1),
31
+ };
32
+ /**
33
+ * Animates a number from `from` to `to` over `duration` ms, calling `onUpdate`
34
+ * every frame. Returns a promise that resolves when the tween is done;
35
+ * `promise.cancel()` stops it early. A duration of 0 or less jumps straight
36
+ * to `to`.
37
+ */
38
+ export function tween({ from, to, duration, ease = easings.linear, onUpdate }) {
39
+ let raf = 0;
40
+ let done = false;
41
+ const promise = new Promise((resolve) => {
42
+ if (duration <= 0) {
43
+ onUpdate(to);
44
+ done = true;
45
+ return resolve();
46
+ }
47
+ const t0 = performance.now();
48
+ const step = (now) => {
49
+ if (done)
50
+ return;
51
+ const k = Math.min(1, (now - t0) / duration);
52
+ onUpdate(from + (to - from) * ease(k));
53
+ if (k < 1)
54
+ raf = requestAnimationFrame(step);
55
+ else {
56
+ done = true;
57
+ resolve();
58
+ }
59
+ };
60
+ raf = requestAnimationFrame(step);
61
+ });
62
+ promise.cancel = () => {
63
+ done = true;
64
+ cancelAnimationFrame(raf);
65
+ };
66
+ return promise;
67
+ }
68
+ export const prefersReducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
69
+ //# sourceMappingURL=animate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate.js","sourceRoot":"","sources":["../src/animate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { type Easing } from './animate.ts';
2
+ /** Looks a custom property up on an element; `undefined` when it is not set. */
3
+ export type CSSVarReader = (name: string) => string | undefined;
4
+ /**
5
+ * Returns a reader over `el`'s computed custom properties. Custom properties
6
+ * inherit, so a variable set on `:root` or any ancestor is visible here. When
7
+ * there is no element or no `getComputedStyle` (SSR, tests), the reader finds
8
+ * nothing and every caller falls back to its JS option.
9
+ */
10
+ export declare function cssVars(el: Element | null | undefined): CSSVarReader;
11
+ /** `500ms`, `0.4s`, or a bare number of milliseconds. */
12
+ export declare function parseTime(v: string | undefined): number | undefined;
13
+ export declare function parseNumber(v: string | undefined): number | undefined;
14
+ /** A fraction. `0.3` and `30%` are equivalent. */
15
+ export declare function parseRatio(v: string | undefined): number | undefined;
16
+ /**
17
+ * A timing keyword or `cubic-bezier(x1, y1, x2, y2)`. The x coordinates must be
18
+ * within [0, 1], as CSS requires. Outside that range the curve is not a
19
+ * function of time and the solver would not converge.
20
+ */
21
+ export declare function parseEasing(v: string | undefined): Easing | undefined;
22
+ //# sourceMappingURL=css-vars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css-vars.d.ts","sourceRoot":"","sources":["../src/css-vars.ts"],"names":[],"mappings":"AASA,OAAO,EAAwB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAEjE,gFAAgF;AAChF,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAOpE;AAID,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAInE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGrE;AAED,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpE;AAuBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAYrE"}
@@ -0,0 +1,89 @@
1
+ // Reads the engine's options from CSS custom properties, so the transition can
2
+ // be tuned from a stylesheet (a media query, a theme class, a single container)
3
+ // instead of only from JS. Values are parsed the way CSS reads them: `300ms`,
4
+ // `0.4s`, `30%`, `cubic-bezier(...)`.
5
+ //
6
+ // Every parser returns undefined for anything it does not understand, never
7
+ // NaN, so an unreadable value falls through to its JS option instead of
8
+ // producing an invalid transform.
9
+ import { cubicBezier, easings } from "./animate.js";
10
+ /**
11
+ * Returns a reader over `el`'s computed custom properties. Custom properties
12
+ * inherit, so a variable set on `:root` or any ancestor is visible here. When
13
+ * there is no element or no `getComputedStyle` (SSR, tests), the reader finds
14
+ * nothing and every caller falls back to its JS option.
15
+ */
16
+ export function cssVars(el) {
17
+ const style = el && typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
18
+ if (!style)
19
+ return () => undefined;
20
+ return (name) => {
21
+ const v = style.getPropertyValue(name)?.trim();
22
+ return v ? v : undefined;
23
+ };
24
+ }
25
+ const NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)$/;
26
+ /** `500ms`, `0.4s`, or a bare number of milliseconds. */
27
+ export function parseTime(v) {
28
+ const m = v === undefined ? null : /^([-+]?(?:\d+\.?\d*|\.\d+))(ms|s)?$/i.exec(v.trim());
29
+ if (!m)
30
+ return undefined;
31
+ return m[2]?.toLowerCase() === 's' ? Number(m[1]) * 1000 : Number(m[1]);
32
+ }
33
+ export function parseNumber(v) {
34
+ const s = v?.trim();
35
+ return s !== undefined && NUMBER.test(s) ? Number(s) : undefined;
36
+ }
37
+ /** A fraction. `0.3` and `30%` are equivalent. */
38
+ export function parseRatio(v) {
39
+ const s = v?.trim();
40
+ if (s === undefined)
41
+ return undefined;
42
+ if (!s.endsWith('%'))
43
+ return parseNumber(s);
44
+ const n = parseNumber(s.slice(0, -1));
45
+ return n === undefined ? undefined : n / 100;
46
+ }
47
+ /**
48
+ * The CSS timing keywords that are cubic curves, plus the two this engine
49
+ * ships. Null-prototype, so `__proto__` and `constructor` miss like any other
50
+ * unknown word instead of returning a truthy non-easing value.
51
+ *
52
+ * Built on first use rather than at module load: solving four curves here
53
+ * would be work a bundler cannot prove pointless, which would keep this
54
+ * module (and `easings`) in bundles that never parse a CSS variable.
55
+ */
56
+ let easingKeywords;
57
+ const easingKeywordsOf = () => (easingKeywords ??= Object.assign(Object.create(null), {
58
+ linear: easings.linear,
59
+ ease: cubicBezier(0.25, 0.1, 0.25, 1),
60
+ 'ease-in': cubicBezier(0.42, 0, 1, 1),
61
+ 'ease-out': cubicBezier(0, 0, 0.58, 1),
62
+ 'ease-in-out': cubicBezier(0.42, 0, 0.58, 1),
63
+ ios: easings.ios,
64
+ 'ios-settle': easings.easeOut,
65
+ }));
66
+ /**
67
+ * A timing keyword or `cubic-bezier(x1, y1, x2, y2)`. The x coordinates must be
68
+ * within [0, 1], as CSS requires. Outside that range the curve is not a
69
+ * function of time and the solver would not converge.
70
+ */
71
+ export function parseEasing(v) {
72
+ if (v === undefined)
73
+ return undefined;
74
+ const s = v.trim().toLowerCase();
75
+ const keyword = easingKeywordsOf()[s];
76
+ if (keyword)
77
+ return keyword;
78
+ const m = /^cubic-bezier\(([^)]*)\)$/.exec(s);
79
+ if (!m)
80
+ return undefined;
81
+ const n = m[1].split(',').map((part) => parseNumber(part));
82
+ if (n.length !== 4 || n.some((x) => x === undefined))
83
+ return undefined;
84
+ const [x1, y1, x2, y2] = n;
85
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1)
86
+ return undefined;
87
+ return cubicBezier(x1, y1, x2, y2);
88
+ }
89
+ //# sourceMappingURL=css-vars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css-vars.js","sourceRoot":"","sources":["../src/css-vars.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Direction resolution: given where the app is and where it is going, decide
3
+ * whether the new page should push over the current one, pop back to it, or
4
+ * replace it. The engine does not decide this on its own. A host supplies an
5
+ * ordered list of strategies, and the first one with an answer wins.
6
+ *
7
+ * Strategies are plain functions, so a host can add its own (a numbering
8
+ * scheme, a route-tree walk, a per-navigation hint) without changing the rest.
9
+ */
10
+ export type Direction = 'push' | 'pop' | 'replace';
11
+ /** What a strategy may return: a direction, or no answer (`'auto'`, `undefined`, `null`). */
12
+ export type DirectionOpinion = Direction | 'auto' | undefined | null | void;
13
+ /** The minimum a strategy needs to know about a page. Hosts may attach more. */
14
+ export interface RouteRef {
15
+ /** stable identity of the page: its URL, a route id, anything unique */
16
+ key: string;
17
+ /** the page's position in the route tree, e.g. URL segments (`['items', '42']`) */
18
+ segments?: readonly string[];
19
+ /** an explicit number when the app numbers its screens: bigger is deeper */
20
+ level?: number | null;
21
+ /** anything the host wants strategies to see (route data, params) */
22
+ data?: Record<string, unknown>;
23
+ }
24
+ export type NavigationTrigger = 'imperative' | 'history';
25
+ export interface NavigationContext {
26
+ /** the page on screen, or null when the stack is empty */
27
+ from: RouteRef | null;
28
+ to: RouteRef;
29
+ /** `imperative`: the app navigated. `history`: the browser's back/forward. */
30
+ trigger?: NavigationTrigger;
31
+ /** for history triggers, when known: negative = back, positive = forward */
32
+ historyDelta?: number;
33
+ /** an explicit direction from the caller, if any */
34
+ hint?: DirectionOpinion;
35
+ /** keys of the pages currently kept alive, bottom to top */
36
+ stack?: readonly string[];
37
+ }
38
+ export type DirectionStrategy = (ctx: NavigationContext) => DirectionOpinion;
39
+ export interface DirectionResolver {
40
+ (ctx: NavigationContext): Direction;
41
+ readonly strategies: readonly DirectionStrategy[];
42
+ readonly fallback: Direction;
43
+ }
44
+ /** Calls each strategy in turn. The first direction returned wins, else `fallback`. */
45
+ export declare function resolveDirection(strategies: readonly DirectionStrategy[], ctx: NavigationContext, fallback?: Direction): Direction;
46
+ /** Bundles strategies and a fallback into a single resolver function. */
47
+ export declare function createDirectionResolver(strategies?: readonly DirectionStrategy[], fallback?: Direction): DirectionResolver;
48
+ /** Honors an explicit per-navigation hint, e.g. `{ info: { stacknav: 'pop' } }` in Angular's router. */
49
+ export declare const fromHint: () => DirectionStrategy;
50
+ /** Browser back is a pop, browser forward is a push. No answer for imperative navigations. */
51
+ export declare const fromHistory: () => DirectionStrategy;
52
+ /** Navigating to a page still kept beneath the current one is a pop back to it. */
53
+ export declare const fromStack: () => DirectionStrategy;
54
+ export interface LevelOptions {
55
+ /** the direction when both pages carry the same number (default `replace`) */
56
+ sameLevel?: DirectionOpinion;
57
+ }
58
+ /**
59
+ * For apps that number their screens (`level: 1`, `level: 2`, …): a higher
60
+ * number pushes, a lower one pops. No answer unless both pages carry a number.
61
+ */
62
+ export declare const fromLevel: ({ sameLevel }?: LevelOptions) => DirectionStrategy;
63
+ export interface TreeOptions {
64
+ /** the direction for two unrelated pages at the same depth, e.g. siblings (default `replace`) */
65
+ sameDepth?: DirectionOpinion;
66
+ }
67
+ /**
68
+ * Reads the route tree: a descendant of the current page pushes, an ancestor
69
+ * pops. Otherwise a deeper page pushes and a shallower one pops. Requires
70
+ * `segments` on both pages.
71
+ */
72
+ export declare const fromTree: ({ sameDepth }?: TreeOptions) => DirectionStrategy;
73
+ /** Always returns the same direction. Useful as the last entry in a list. */
74
+ export declare const always: (direction: Direction) => DirectionStrategy;
75
+ /** The default order: an explicit hint, then browser history, then the kept stack, then numbering, then the tree. */
76
+ export declare const defaultStrategies: () => DirectionStrategy[];
77
+ /** Split a URL path into segments, ignoring the query, fragment and empty parts. */
78
+ export declare function segmentsOf(url: string): string[];
79
+ //# sourceMappingURL=direction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direction.d.ts","sourceRoot":"","sources":["../src/direction.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAEnD,6FAA6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5E,gFAAgF;AAChF,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,EAAE,EAAE,QAAQ,CAAC;IACb,8EAA8E;IAC9E,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,iBAAiB,KAAK,gBAAgB,CAAC;AAE7E,MAAM,WAAW,iBAAiB;IAChC,CAAC,GAAG,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B;AAID,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,EAAE,GAAG,EAAE,iBAAiB,EAAE,QAAQ,GAAE,SAAkB,GAAG,SAAS,CAM1I;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CAAC,UAAU,GAAE,SAAS,iBAAiB,EAAwB,EAAE,QAAQ,GAAE,SAAkB,GAAG,iBAAiB,CAKvJ;AAID,wGAAwG;AACxG,eAAO,MAAM,QAAQ,QAAO,iBAAsC,CAAC;AAEnE,8FAA8F;AAC9F,eAAO,MAAM,WAAW,QAAO,iBAG9B,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,SAAS,QAAO,iBAM5B,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,gBAA2B,YAAiB,KAAG,iBAOxE,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,iGAAiG;IACjG,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,gBAA2B,WAAgB,KAAG,iBAStE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,MAAM,GAAI,WAAW,SAAS,KAAG,iBAAoC,CAAC;AAEnF,qHAAqH;AACrH,eAAO,MAAM,iBAAiB,QAAO,iBAAiB,EAAuE,CAAC;AAQ9H,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAGhD"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Direction resolution: given where the app is and where it is going, decide
3
+ * whether the new page should push over the current one, pop back to it, or
4
+ * replace it. The engine does not decide this on its own. A host supplies an
5
+ * ordered list of strategies, and the first one with an answer wins.
6
+ *
7
+ * Strategies are plain functions, so a host can add its own (a numbering
8
+ * scheme, a route-tree walk, a per-navigation hint) without changing the rest.
9
+ */
10
+ const isDirection = (v) => v === 'push' || v === 'pop' || v === 'replace';
11
+ /** Calls each strategy in turn. The first direction returned wins, else `fallback`. */
12
+ export function resolveDirection(strategies, ctx, fallback = 'push') {
13
+ for (const s of strategies) {
14
+ const d = s(ctx);
15
+ if (isDirection(d))
16
+ return d;
17
+ }
18
+ return fallback;
19
+ }
20
+ /** Bundles strategies and a fallback into a single resolver function. */
21
+ export function createDirectionResolver(strategies = defaultStrategies(), fallback = 'push') {
22
+ const resolver = ((ctx) => resolveDirection(strategies, ctx, fallback));
23
+ Object.defineProperty(resolver, 'strategies', { value: strategies.slice(), enumerable: true });
24
+ Object.defineProperty(resolver, 'fallback', { value: fallback, enumerable: true });
25
+ return resolver;
26
+ }
27
+ // ------------------------------------------------------------------ strategies
28
+ /** Honors an explicit per-navigation hint, e.g. `{ info: { stacknav: 'pop' } }` in Angular's router. */
29
+ export const fromHint = () => (ctx) => ctx.hint;
30
+ /** Browser back is a pop, browser forward is a push. No answer for imperative navigations. */
31
+ export const fromHistory = () => (ctx) => {
32
+ if (ctx.trigger !== 'history' || !ctx.historyDelta)
33
+ return undefined;
34
+ return ctx.historyDelta < 0 ? 'pop' : 'push';
35
+ };
36
+ /** Navigating to a page still kept beneath the current one is a pop back to it. */
37
+ export const fromStack = () => (ctx) => {
38
+ const stack = ctx.stack;
39
+ if (!stack || stack.length < 2)
40
+ return undefined;
41
+ const i = stack.lastIndexOf(ctx.to.key);
42
+ if (i < 0 || i === stack.length - 1)
43
+ return undefined;
44
+ return 'pop';
45
+ };
46
+ /**
47
+ * For apps that number their screens (`level: 1`, `level: 2`, …): a higher
48
+ * number pushes, a lower one pops. No answer unless both pages carry a number.
49
+ */
50
+ export const fromLevel = ({ sameLevel = 'replace' } = {}) => (ctx) => {
51
+ const a = ctx.from?.level;
52
+ const b = ctx.to.level;
53
+ if (typeof a !== 'number' || typeof b !== 'number')
54
+ return undefined;
55
+ if (b > a)
56
+ return 'push';
57
+ if (b < a)
58
+ return 'pop';
59
+ return sameLevel;
60
+ };
61
+ /**
62
+ * Reads the route tree: a descendant of the current page pushes, an ancestor
63
+ * pops. Otherwise a deeper page pushes and a shallower one pops. Requires
64
+ * `segments` on both pages.
65
+ */
66
+ export const fromTree = ({ sameDepth = 'replace' } = {}) => (ctx) => {
67
+ const a = ctx.from?.segments;
68
+ const b = ctx.to.segments;
69
+ if (!a || !b)
70
+ return undefined;
71
+ if (isPrefix(a, b))
72
+ return b.length > a.length ? 'push' : sameDepth;
73
+ if (isPrefix(b, a))
74
+ return 'pop';
75
+ if (b.length > a.length)
76
+ return 'push';
77
+ if (b.length < a.length)
78
+ return 'pop';
79
+ return sameDepth;
80
+ };
81
+ /** Always returns the same direction. Useful as the last entry in a list. */
82
+ export const always = (direction) => () => direction;
83
+ /** The default order: an explicit hint, then browser history, then the kept stack, then numbering, then the tree. */
84
+ export const defaultStrategies = () => [fromHint(), fromHistory(), fromStack(), fromLevel(), fromTree()];
85
+ function isPrefix(prefix, of) {
86
+ if (prefix.length > of.length)
87
+ return false;
88
+ for (let i = 0; i < prefix.length; i++)
89
+ if (prefix[i] !== of[i])
90
+ return false;
91
+ return true;
92
+ }
93
+ /** Split a URL path into segments, ignoring the query, fragment and empty parts. */
94
+ export function segmentsOf(url) {
95
+ const path = url.split(/[?#]/, 1)[0];
96
+ return path.split('/').filter(Boolean);
97
+ }
98
+ //# sourceMappingURL=direction.js.map