@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.
- package/README.md +235 -0
- package/dist/animate.d.ts +26 -0
- package/dist/animate.d.ts.map +1 -0
- package/dist/animate.js +69 -0
- package/dist/animate.js.map +1 -0
- package/dist/css-vars.d.ts +22 -0
- package/dist/css-vars.d.ts.map +1 -0
- package/dist/css-vars.js +89 -0
- package/dist/css-vars.js.map +1 -0
- package/dist/direction.d.ts +79 -0
- package/dist/direction.d.ts.map +1 -0
- package/dist/direction.js +98 -0
- package/dist/direction.js.map +1 -0
- package/dist/edge-pan-gesture.d.ts +33 -0
- package/dist/edge-pan-gesture.d.ts.map +1 -0
- package/dist/edge-pan-gesture.js +128 -0
- package/dist/edge-pan-gesture.js.map +1 -0
- package/dist/history-adapter.d.ts +17 -0
- package/dist/history-adapter.d.ts.map +1 -0
- package/dist/history-adapter.js +44 -0
- package/dist/history-adapter.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/ios-transition.d.ts +50 -0
- package/dist/ios-transition.d.ts.map +1 -0
- package/dist/ios-transition.js +122 -0
- package/dist/ios-transition.js.map +1 -0
- package/dist/navigation-stack.d.ts +177 -0
- package/dist/navigation-stack.d.ts.map +1 -0
- package/dist/navigation-stack.js +296 -0
- package/dist/navigation-stack.js.map +1 -0
- package/dist/stacknav.css +33 -0
- package/dist/styles.d.ts +29 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +39 -0
- package/dist/styles.js.map +1 -0
- package/package.json +43 -0
- package/src/animate.ts +80 -0
- package/src/css-vars.ts +91 -0
- package/src/direction.ts +146 -0
- package/src/edge-pan-gesture.ts +162 -0
- package/src/history-adapter.ts +52 -0
- package/src/index.ts +86 -0
- package/src/ios-transition.ts +163 -0
- package/src/navigation-stack.ts +362 -0
- 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"}
|
package/dist/animate.js
ADDED
|
@@ -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"}
|
package/dist/css-vars.js
ADDED
|
@@ -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
|