@octanejs/spring 0.0.1
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/LICENSE +21 -0
- package/README.md +53 -0
- package/UPSTREAM.md +103 -0
- package/package.json +73 -0
- package/src/animated/Animated.ts +24 -0
- package/src/animated/AnimatedArray.ts +26 -0
- package/src/animated/index.ts +3 -0
- package/src/browser.ts +246 -0
- package/src/components.tsrx +60 -0
- package/src/components.tsrx.d.ts +33 -0
- package/src/context.ts +13 -0
- package/src/core/AnimationConfig.ts +60 -0
- package/src/core/AnimationResult.ts +19 -0
- package/src/core/Controller.ts +200 -0
- package/src/core/FrameValue.ts +16 -0
- package/src/core/Interpolation.ts +46 -0
- package/src/core/SpringRef.ts +26 -0
- package/src/core/SpringValue.ts +384 -0
- package/src/core/index.ts +9 -0
- package/src/core/runAsync.ts +17 -0
- package/src/core/scheduleProps.ts +34 -0
- package/src/core/valueInterpolation.ts +54 -0
- package/src/engine.ts +9 -0
- package/src/hooks.ts +610 -0
- package/src/index.ts +26 -0
- package/src/parallax.ts +8 -0
- package/src/parallax.tsrx +321 -0
- package/src/parallax.tsrx.d.ts +60 -0
- package/src/shared/FrameLoop.ts +75 -0
- package/src/shared/createInterpolator.ts +85 -0
- package/src/shared/globals.ts +7 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/stringInterpolation.ts +56 -0
- package/src/spring-component.ts +36 -0
- package/src/types/animated.ts +10 -0
- package/src/types/common.ts +29 -0
- package/src/types/fluid.ts +9 -0
- package/src/types/index.ts +9 -0
- package/src/types/interpolate.ts +50 -0
- package/src/types/interpolation.ts +18 -0
- package/src/types/objects.ts +17 -0
- package/src/types/props.ts +275 -0
- package/src/types/utils.ts +57 -0
- package/src/upstream-compat.ts +164 -0
- package/src/web/AnimatedStyle.ts +51 -0
- package/src/web/animated.ts +118 -0
- package/src/web/applyAnimatedValues.ts +92 -0
- package/src/web/index.ts +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @octanejs/spring
|
|
2
|
+
|
|
3
|
+
[React Spring](https://www.react-spring.dev/) for the
|
|
4
|
+
[Octane](https://github.com/octanejs/octane) UI framework.
|
|
5
|
+
|
|
6
|
+
The package exposes the stable `@react-spring/web@10.1.2` surface at its root
|
|
7
|
+
and Parallax from `@octanejs/spring/parallax`. It reuses React Spring's
|
|
8
|
+
`rafz` scheduler and ports the framework-bound controller, hook, animated-host,
|
|
9
|
+
observer, and component layers to Octane without a React runtime dependency.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { animated, useSpring } from '@octanejs/spring';
|
|
13
|
+
|
|
14
|
+
export function Card(props) @{
|
|
15
|
+
const [styles, api] = useSpring(
|
|
16
|
+
() => ({
|
|
17
|
+
from: { opacity: 0, y: 16 },
|
|
18
|
+
to: { opacity: 1, y: 0 },
|
|
19
|
+
}),
|
|
20
|
+
[],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
<animated.div style={styles} onClick={() => api.start({ to: { y: 8 } })}>
|
|
24
|
+
{props.children}
|
|
25
|
+
</animated.div>
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Upstream
|
|
30
|
+
|
|
31
|
+
The behavioral and type baseline is React Spring `v10.1.2` at commit
|
|
32
|
+
`59b1e5306402d3039120e2da464b66e10b1a1aa1`. Adapted source provenance is
|
|
33
|
+
recorded in [`UPSTREAM.md`](./UPSTREAM.md).
|
|
34
|
+
|
|
35
|
+
## Status
|
|
36
|
+
|
|
37
|
+
Supported scope, divergences, and verification are recorded in
|
|
38
|
+
[`status.json`](./status.json) and the generated repository compatibility
|
|
39
|
+
tables.
|
|
40
|
+
|
|
41
|
+
## Verification
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm --dir packages/spring upstream:verify
|
|
45
|
+
pnpm --dir packages/spring typecheck
|
|
46
|
+
pnpm exec vitest run --project spring --project spring-ssr
|
|
47
|
+
pnpm exec vitest run --project spring-browser
|
|
48
|
+
pnpm packages:pack:check
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The browser lane builds and exercises the central playground in development
|
|
52
|
+
and production. The repository packed-source canary installs the root and
|
|
53
|
+
Parallax subpath from their tarball alongside a packed Octane runtime.
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# React Spring upstream contract
|
|
2
|
+
|
|
3
|
+
## Pin and source boundary
|
|
4
|
+
|
|
5
|
+
| Field | Value |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Packages | `@react-spring/web@10.1.2`, `@react-spring/parallax@10.1.2` |
|
|
8
|
+
| Supported upstream range | exactly `10.1.2` |
|
|
9
|
+
| Canonical repository | `https://github.com/pmndrs/react-spring.git` |
|
|
10
|
+
| Tag and commit | `v10.1.2`, `59b1e5306402d3039120e2da464b66e10b1a1aa1` |
|
|
11
|
+
| Runtime oracle | React `19.2.7`, ReactDOM `19.2.7` |
|
|
12
|
+
| Framework-neutral dependency | `@react-spring/rafz@10.1.2` |
|
|
13
|
+
| License | MIT, © Paul Henschel and React Spring contributors |
|
|
14
|
+
| Vendored inventory | 167 files; `SHA256SUMS` digest `10e6fb1c530efe9409d9f3184488bc7d57bcbae3c92ee97553859e4cd0eda993` |
|
|
15
|
+
|
|
16
|
+
The npm artifacts contain compiled JavaScript, declarations, README files, and
|
|
17
|
+
licenses, but not the canonical TypeScript source or test suites. The
|
|
18
|
+
byte-exact source, runtime tests, type tests, package manifests, web target,
|
|
19
|
+
Parallax demo, and licenses therefore come from the canonical repository at the
|
|
20
|
+
tag commit. They live under `upstream/`, retain the repository layout, are
|
|
21
|
+
excluded from the published `files`, and are locked file-by-file by
|
|
22
|
+
`upstream/SHA256SUMS`.
|
|
23
|
+
|
|
24
|
+
Run `pnpm --dir packages/spring upstream:verify` to detect a modified,
|
|
25
|
+
missing, renamed, or unexpected vendored file. The verifier itself has negative
|
|
26
|
+
controls in `scripts/react-parity/react-spring-upstream-lib.test.mjs`.
|
|
27
|
+
|
|
28
|
+
The reusable runtime boundary is deliberately narrow: the port consumes the
|
|
29
|
+
exact framework-neutral `@react-spring/rafz` package. Source under upstream
|
|
30
|
+
`packages/shared`, `packages/animated`, `packages/core`, `targets/web`, and
|
|
31
|
+
`packages/parallax` contains React hooks, contexts, JSX, element types, or
|
|
32
|
+
`forwardRef`, so those observable contracts are adapted to Octane. The
|
|
33
|
+
`packages/types` tree is the type oracle. Adapted source cites the pinned
|
|
34
|
+
release, while the machine-checked test disposition inventory ties every
|
|
35
|
+
upstream test file to its executable Octane evidence.
|
|
36
|
+
|
|
37
|
+
## Runtime export crosswalk
|
|
38
|
+
|
|
39
|
+
`tests/conformance/exports.test.ts` enforces this inventory in both directions.
|
|
40
|
+
“Ported” means the public value exists in the Octane namespace; behavioral and
|
|
41
|
+
type evidence is tracked separately and an export is not a release-ready claim
|
|
42
|
+
until its cited lanes pass.
|
|
43
|
+
|
|
44
|
+
| Upstream export | Disposition | Evidence |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `Any` | Ported type sentinel | export inventory and `typetests/public-api.test-d.tsx` |
|
|
47
|
+
| `BailSignal` | Ported interruption signal | export inventory and lifecycle conformance |
|
|
48
|
+
| `Controller` | Ported | engine, controller, and lifecycle conformance |
|
|
49
|
+
| `FrameValue` | Ported public base | animated graph and host conformance |
|
|
50
|
+
| `Globals` | Ported global configuration | reduced-motion and browser conformance |
|
|
51
|
+
| `Interpolation` | Ported | interpolation and animated graph conformance |
|
|
52
|
+
| `Spring` | Ported to Octane render-prop component | `tests/conformance/components.test.ts` |
|
|
53
|
+
| `SpringContext` | Ported to Octane context component | `tests/conformance/components.test.ts` |
|
|
54
|
+
| `SpringRef` | Ported | `tests/conformance/hooks.test.ts` |
|
|
55
|
+
| `SpringValue` | Ported | engine, advanced-engine, lifecycle, and public type lanes |
|
|
56
|
+
| `Trail` | Ported to keyed Octane renderables | `tests/conformance/components.test.ts` |
|
|
57
|
+
| `Transition` | Ported to keyed Octane renderables | `tests/conformance/components.test.ts` |
|
|
58
|
+
| `a` | Ported alias | export inventory and animated-host fixture |
|
|
59
|
+
| `animated` | Ported to Octane host components | `tests/conformance/prerequisite-seams.test.ts` |
|
|
60
|
+
| `config` | Ported | `tests/conformance/engine.test.ts` |
|
|
61
|
+
| `createInterpolator` | Ported | interpolation conformance |
|
|
62
|
+
| `easings` | Ported | advanced-engine and export conformance |
|
|
63
|
+
| `inferTo` | Ported | export and public type conformance |
|
|
64
|
+
| `interpolate` | Ported alias | `tests/conformance/engine.test.ts` |
|
|
65
|
+
| `to` | Ported | `tests/conformance/engine.test.ts` |
|
|
66
|
+
| `update` | Reused from exact `rafz` | controlled frame-loop conformance |
|
|
67
|
+
| `useChain` | Ported to Octane hooks | `tests/conformance/hooks.test.ts` |
|
|
68
|
+
| `useInView` | Ported to native observer lifecycle | `tests/conformance/browser-hooks.test.ts` |
|
|
69
|
+
| `useIsomorphicLayoutEffect` | Ported to Octane layout effect | SSR, hydration, and browser-hook lanes |
|
|
70
|
+
| `useReducedMotion` | Ported to native media-query lifecycle | `tests/conformance/browser-hooks.test.ts` |
|
|
71
|
+
| `useResize` | Ported to native observer lifecycle | `tests/conformance/browser-hooks.test.ts` |
|
|
72
|
+
| `useScroll` | Ported to native scroll lifecycle | `tests/conformance/browser-hooks.test.ts` |
|
|
73
|
+
| `useSpring` | Ported to Octane hooks | `tests/conformance/hooks.test.ts` |
|
|
74
|
+
| `useSpringRef` | Ported to Octane hooks | `tests/conformance/hooks.test.ts` |
|
|
75
|
+
| `useSpringValue` | Ported to Octane hooks | `tests/conformance/hooks.test.ts` |
|
|
76
|
+
| `useSprings` | Ported to Octane hooks | `tests/conformance/hooks.test.ts` |
|
|
77
|
+
| `useTrail` | Ported to Octane hooks | `tests/conformance/hooks.test.ts` |
|
|
78
|
+
| `useTransition` | Ported with binding-owned keyed retention | `tests/conformance/transitions.test.ts` |
|
|
79
|
+
| `Parallax` | Ported at `./parallax` | `tests/conformance/parallax.test.ts` |
|
|
80
|
+
| `ParallaxLayer` | Ported at `./parallax` | `tests/conformance/parallax.test.ts` |
|
|
81
|
+
|
|
82
|
+
`useSpringContext` remains a private helper and is intentionally absent from
|
|
83
|
+
the package namespace. Native, Three, Konva, Zdog, and the all-renderer
|
|
84
|
+
`react-spring` meta-package are outside this web binding rather than missing web
|
|
85
|
+
exports.
|
|
86
|
+
|
|
87
|
+
## Upstream test-suite disposition
|
|
88
|
+
|
|
89
|
+
The pinned boundary contains the executable unit/type-test files under
|
|
90
|
+
`packages/{animated,core,rafz,shared}` and `targets/web`, plus setup and the
|
|
91
|
+
Parallax demo. The vendored files are the authoritative work list.
|
|
92
|
+
|
|
93
|
+
Runtime identities are inventoried by the pristine Vitest lane and mapped
|
|
94
|
+
one-for-one in `audit/upstream-case-dispositions.json` to an adapted identity,
|
|
95
|
+
a reused-dependency note, or an explicit `awaiting-adaptation` reason. Negative
|
|
96
|
+
controls reject missing/extra pristine identities, adapted title drift, and
|
|
97
|
+
skipped adapted cases. Repo-authored conformance/hydration/browser suites stay
|
|
98
|
+
in the ordinary shards and are not React-parity ownership.
|
|
99
|
+
|
|
100
|
+
Type programs under `*.test-d.ts` / `*.test-d.tsx` run pristine via
|
|
101
|
+
`audit/type-probes/tsconfig.pristine-upstream.json`. One-for-one adapted type
|
|
102
|
+
counterparts live under `typetests/upstream/` and still require Octane public
|
|
103
|
+
type-surface work before the adapted-types lane is green.
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/spring",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.22.2"
|
|
8
|
+
},
|
|
9
|
+
"octane": {
|
|
10
|
+
"hookSlots": {
|
|
11
|
+
"manual": [
|
|
12
|
+
"src"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"description": "React Spring web bindings for Octane.",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Dominic Gannaway",
|
|
19
|
+
"email": "dg@domgan.com"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
27
|
+
"directory": "packages/spring"
|
|
28
|
+
},
|
|
29
|
+
"main": "src/index.ts",
|
|
30
|
+
"module": "src/index.ts",
|
|
31
|
+
"types": "src/index.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"README.md",
|
|
35
|
+
"UPSTREAM.md"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": "./src/index.ts",
|
|
39
|
+
"./parallax": "./src/parallax.ts"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@react-spring/rafz": "10.1.2"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"octane": "0.1.35"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@react-spring/animated": "10.1.2",
|
|
49
|
+
"@react-spring/core": "10.1.2",
|
|
50
|
+
"@react-spring/mock-raf": "1.1.1",
|
|
51
|
+
"@react-spring/parallax": "10.1.2",
|
|
52
|
+
"@react-spring/shared": "10.1.2",
|
|
53
|
+
"@react-spring/types": "10.1.2",
|
|
54
|
+
"@react-spring/web": "^10.1.2",
|
|
55
|
+
"@types/react": "^19.2.17",
|
|
56
|
+
"@types/react-dom": "^19.2.3",
|
|
57
|
+
"@vitest/browser-playwright": "4.1.10",
|
|
58
|
+
"flush-microtasks": "1.0.1",
|
|
59
|
+
"playwright": "^1.61.1",
|
|
60
|
+
"react": "19.2.7",
|
|
61
|
+
"react-dom": "19.2.7",
|
|
62
|
+
"spec.ts": "^1.1.3",
|
|
63
|
+
"vite": "^8.1.5",
|
|
64
|
+
"vitest": "^4.1.10",
|
|
65
|
+
"vitest-browser-react": "^2.2.0",
|
|
66
|
+
"octane": "0.1.35"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"test": "pnpm upstream:verify && vitest run --project spring",
|
|
70
|
+
"upstream:verify": "node ../../scripts/react-parity/react-spring-upstream.mjs",
|
|
71
|
+
"typecheck": "tsrx-tsc --noEmit -p tsconfig.json && tsgo --noEmit -p typetests/tsconfig.json"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// React-free graph node ported from react-spring v10.1.2 packages/animated/src/Animated.ts.
|
|
2
|
+
export abstract class Animated {
|
|
3
|
+
getPayload(): AnimatedValue<any>[] | undefined {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
abstract getValue(): unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AnimatedValue<T = number> extends Animated {
|
|
10
|
+
constructor(private value: T) {
|
|
11
|
+
super();
|
|
12
|
+
}
|
|
13
|
+
getValue(): T {
|
|
14
|
+
return this.value;
|
|
15
|
+
}
|
|
16
|
+
setValue(value: T): boolean {
|
|
17
|
+
if (Object.is(value, this.value)) return false;
|
|
18
|
+
this.value = value;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
getPayload(): AnimatedValue<T>[] {
|
|
22
|
+
return [this];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Ported from react-spring v10.1.2 packages/animated/src/AnimatedArray.ts.
|
|
2
|
+
import { Animated, AnimatedValue } from './Animated';
|
|
3
|
+
|
|
4
|
+
export class AnimatedArray<T = unknown> extends Animated {
|
|
5
|
+
readonly source: Array<AnimatedValue<T>>;
|
|
6
|
+
constructor(source: readonly T[]) {
|
|
7
|
+
super();
|
|
8
|
+
this.source = source.map((value) => new AnimatedValue(value));
|
|
9
|
+
}
|
|
10
|
+
getValue(): T[] {
|
|
11
|
+
return this.source.map((value) => value.getValue());
|
|
12
|
+
}
|
|
13
|
+
setValue(values: readonly T[]): boolean {
|
|
14
|
+
let changed = values.length !== this.source.length;
|
|
15
|
+
while (this.source.length < values.length)
|
|
16
|
+
this.source.push(new AnimatedValue(values[this.source.length]!));
|
|
17
|
+
this.source.length = values.length;
|
|
18
|
+
values.forEach((value, index) => {
|
|
19
|
+
changed = this.source[index]!.setValue(value) || changed;
|
|
20
|
+
});
|
|
21
|
+
return changed;
|
|
22
|
+
}
|
|
23
|
+
getPayload(): AnimatedValue<T>[] {
|
|
24
|
+
return this.source;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Behavioral port of React Spring v10.1.2 browser hooks.
|
|
2
|
+
import { useLayoutEffect, useState } from 'octane';
|
|
3
|
+
import { SpringValue, type ControllerUpdate } from './engine';
|
|
4
|
+
import { Globals } from './shared/globals';
|
|
5
|
+
|
|
6
|
+
type ElementRef<T> = { current: T | null };
|
|
7
|
+
|
|
8
|
+
export const useIsomorphicLayoutEffect = useLayoutEffect;
|
|
9
|
+
const browserSubCache = new Map<symbol, Map<string, symbol>>();
|
|
10
|
+
|
|
11
|
+
function browserSub(slot: symbol | undefined, tag: string): symbol | undefined {
|
|
12
|
+
if (slot === undefined) return undefined;
|
|
13
|
+
let tags = browserSubCache.get(slot);
|
|
14
|
+
if (tags === undefined) browserSubCache.set(slot, (tags = new Map()));
|
|
15
|
+
let value = tags.get(tag);
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
value = Symbol.for(`${slot.description ?? 'react-spring-browser'}:${tag}`);
|
|
18
|
+
tags.set(tag, value);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function browserSlot(args: any[]): symbol | undefined {
|
|
24
|
+
const tail = args[args.length - 1];
|
|
25
|
+
return typeof tail === 'symbol' ? tail : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readScroll(target: Window | HTMLElement) {
|
|
29
|
+
const element = target instanceof Window ? document.documentElement : target;
|
|
30
|
+
const x = target instanceof Window ? target.scrollX : target.scrollLeft;
|
|
31
|
+
const y = target instanceof Window ? target.scrollY : target.scrollTop;
|
|
32
|
+
const width =
|
|
33
|
+
target instanceof Window
|
|
34
|
+
? element.scrollWidth - target.innerWidth
|
|
35
|
+
: element.scrollWidth - element.clientWidth;
|
|
36
|
+
const height =
|
|
37
|
+
target instanceof Window
|
|
38
|
+
? element.scrollHeight - target.innerHeight
|
|
39
|
+
: element.scrollHeight - element.clientHeight;
|
|
40
|
+
return {
|
|
41
|
+
scrollX: x,
|
|
42
|
+
scrollY: y,
|
|
43
|
+
scrollXProgress: width > 0 ? x / width : 0,
|
|
44
|
+
scrollYProgress: height > 0 ? y / height : 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useScroll(
|
|
49
|
+
optionsOrSlot: (ControllerUpdate<any> & { container?: ElementRef<HTMLElement> }) | symbol = {},
|
|
50
|
+
...args: any[]
|
|
51
|
+
) {
|
|
52
|
+
const slot = typeof optionsOrSlot === 'symbol' ? optionsOrSlot : browserSlot(args);
|
|
53
|
+
const options = typeof optionsOrSlot === 'symbol' ? {} : optionsOrSlot;
|
|
54
|
+
const [values] = useState(
|
|
55
|
+
() => ({
|
|
56
|
+
scrollX: new SpringValue(0),
|
|
57
|
+
scrollY: new SpringValue(0),
|
|
58
|
+
scrollXProgress: new SpringValue(0),
|
|
59
|
+
scrollYProgress: new SpringValue(0),
|
|
60
|
+
}),
|
|
61
|
+
browserSub(slot, 'scroll-values'),
|
|
62
|
+
);
|
|
63
|
+
useLayoutEffect(
|
|
64
|
+
() => {
|
|
65
|
+
if (typeof window === 'undefined') return;
|
|
66
|
+
const target = options.container?.current ?? window;
|
|
67
|
+
const update = () => {
|
|
68
|
+
const next = readScroll(target);
|
|
69
|
+
for (const key of Object.keys(next) as Array<keyof typeof next>) {
|
|
70
|
+
void values[key].start({
|
|
71
|
+
to: next[key],
|
|
72
|
+
config: typeof options.config === 'function' ? options.config(key) : options.config,
|
|
73
|
+
immediate:
|
|
74
|
+
typeof options.immediate === 'function' ? options.immediate(key) : options.immediate,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
update();
|
|
79
|
+
target.addEventListener('scroll', update, { passive: true });
|
|
80
|
+
return () => {
|
|
81
|
+
target.removeEventListener('scroll', update);
|
|
82
|
+
Object.values(values).forEach((value) => value.stop(true));
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
[options.container?.current],
|
|
86
|
+
browserSub(slot, 'scroll-effect'),
|
|
87
|
+
);
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useResize(
|
|
92
|
+
optionsOrSlot: (ControllerUpdate<any> & { container?: ElementRef<HTMLElement> }) | symbol = {},
|
|
93
|
+
...args: any[]
|
|
94
|
+
) {
|
|
95
|
+
const slot = typeof optionsOrSlot === 'symbol' ? optionsOrSlot : browserSlot(args);
|
|
96
|
+
const options = typeof optionsOrSlot === 'symbol' ? {} : optionsOrSlot;
|
|
97
|
+
const [values] = useState(
|
|
98
|
+
() => ({
|
|
99
|
+
width: new SpringValue(0),
|
|
100
|
+
height: new SpringValue(0),
|
|
101
|
+
}),
|
|
102
|
+
browserSub(slot, 'resize-values'),
|
|
103
|
+
);
|
|
104
|
+
useLayoutEffect(
|
|
105
|
+
() => {
|
|
106
|
+
if (typeof window === 'undefined') return;
|
|
107
|
+
const element = options.container?.current;
|
|
108
|
+
const update = (width: number, height: number) => {
|
|
109
|
+
for (const [key, value] of [
|
|
110
|
+
['width', width],
|
|
111
|
+
['height', height],
|
|
112
|
+
] as const) {
|
|
113
|
+
void values[key].start({
|
|
114
|
+
to: value,
|
|
115
|
+
config: typeof options.config === 'function' ? options.config(key) : options.config,
|
|
116
|
+
immediate:
|
|
117
|
+
typeof options.immediate === 'function' ? options.immediate(key) : options.immediate,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const stop = () => {
|
|
122
|
+
values.width.stop(true);
|
|
123
|
+
values.height.stop(true);
|
|
124
|
+
};
|
|
125
|
+
if (element === undefined || element === null) {
|
|
126
|
+
const onResize = () => update(window.innerWidth, window.innerHeight);
|
|
127
|
+
onResize();
|
|
128
|
+
window.addEventListener('resize', onResize);
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener('resize', onResize);
|
|
131
|
+
stop();
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const rect = element.getBoundingClientRect();
|
|
135
|
+
update(rect.width, rect.height);
|
|
136
|
+
if (typeof ResizeObserver === 'undefined') return stop;
|
|
137
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
138
|
+
if (entry !== undefined) update(entry.contentRect.width, entry.contentRect.height);
|
|
139
|
+
});
|
|
140
|
+
observer.observe(element);
|
|
141
|
+
return () => {
|
|
142
|
+
observer.disconnect();
|
|
143
|
+
stop();
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
[options.container?.current],
|
|
147
|
+
browserSub(slot, 'resize-effect'),
|
|
148
|
+
);
|
|
149
|
+
return values;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface InViewOptions extends Omit<IntersectionObserverInit, 'root' | 'threshold'> {
|
|
153
|
+
root?: ElementRef<HTMLElement>;
|
|
154
|
+
once?: boolean;
|
|
155
|
+
amount?: 'any' | 'all' | number | number[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createInViewRef(setTarget: (target: Element | null) => void): ElementRef<Element> {
|
|
159
|
+
const state = { value: null as Element | null };
|
|
160
|
+
return {
|
|
161
|
+
get current() {
|
|
162
|
+
return state.value;
|
|
163
|
+
},
|
|
164
|
+
set current(next) {
|
|
165
|
+
if (state.value !== next) {
|
|
166
|
+
state.value = next;
|
|
167
|
+
setTarget(next);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useInView(
|
|
174
|
+
optionsOrSlot: InViewOptions | symbol = {},
|
|
175
|
+
...args: any[]
|
|
176
|
+
): [ElementRef<Element>, boolean] {
|
|
177
|
+
const slot = typeof optionsOrSlot === 'symbol' ? optionsOrSlot : browserSlot(args);
|
|
178
|
+
const options = typeof optionsOrSlot === 'symbol' ? {} : optionsOrSlot;
|
|
179
|
+
const [target, setTarget] = useState<Element | null>(null, browserSub(slot, 'in-view-target'));
|
|
180
|
+
const [ref] = useState<ElementRef<Element>>(
|
|
181
|
+
function () {
|
|
182
|
+
return createInViewRef(setTarget);
|
|
183
|
+
},
|
|
184
|
+
browserSub(slot, 'in-view-ref'),
|
|
185
|
+
);
|
|
186
|
+
const [inView, setInView] = useState(false, browserSub(slot, 'in-view-value'));
|
|
187
|
+
useLayoutEffect(
|
|
188
|
+
function () {
|
|
189
|
+
if (
|
|
190
|
+
typeof IntersectionObserver === 'undefined' ||
|
|
191
|
+
target === null ||
|
|
192
|
+
(options.once && inView)
|
|
193
|
+
) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const { amount = 'any', once, root, ...observerOptions } = options;
|
|
197
|
+
const threshold = amount === 'any' ? 0 : amount === 'all' ? 1 : amount;
|
|
198
|
+
const observer = new IntersectionObserver(
|
|
199
|
+
function ([entry]) {
|
|
200
|
+
if (entry === undefined) return;
|
|
201
|
+
setInView(entry.isIntersecting);
|
|
202
|
+
if (entry.isIntersecting && once) observer.disconnect();
|
|
203
|
+
},
|
|
204
|
+
{ ...observerOptions, root: root?.current ?? null, threshold },
|
|
205
|
+
);
|
|
206
|
+
observer.observe(target);
|
|
207
|
+
return function () {
|
|
208
|
+
observer.disconnect();
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
[target, options.root?.current, options.once, options.amount, options.rootMargin],
|
|
212
|
+
browserSub(slot, 'in-view-effect'),
|
|
213
|
+
);
|
|
214
|
+
return [ref, inView];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function useReducedMotion(...args: any[]): boolean | null {
|
|
218
|
+
const slot = browserSlot(args);
|
|
219
|
+
const [reduced, setReduced] = useState<boolean | null>(
|
|
220
|
+
() =>
|
|
221
|
+
typeof window === 'undefined' || typeof window.matchMedia !== 'function'
|
|
222
|
+
? null
|
|
223
|
+
: window.matchMedia('(prefers-reduced-motion)').matches,
|
|
224
|
+
browserSub(slot, 'reduced-value'),
|
|
225
|
+
);
|
|
226
|
+
useLayoutEffect(
|
|
227
|
+
() => {
|
|
228
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
229
|
+
const query = window.matchMedia('(prefers-reduced-motion)');
|
|
230
|
+
const update = () => {
|
|
231
|
+
setReduced(query.matches);
|
|
232
|
+
Globals.assign({ skipAnimation: query.matches });
|
|
233
|
+
};
|
|
234
|
+
update();
|
|
235
|
+
if (query.addEventListener) query.addEventListener('change', update);
|
|
236
|
+
else query.addListener(update);
|
|
237
|
+
return () => {
|
|
238
|
+
if (query.removeEventListener) query.removeEventListener('change', update);
|
|
239
|
+
else query.removeListener(update);
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
[],
|
|
243
|
+
browserSub(slot, 'reduced-effect'),
|
|
244
|
+
);
|
|
245
|
+
return reduced;
|
|
246
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createElement, Fragment, isChildrenBlock } from 'octane';
|
|
2
|
+
import {
|
|
3
|
+
SpringContext as SpringContextObject,
|
|
4
|
+
type SpringContextValue,
|
|
5
|
+
useSpringContext,
|
|
6
|
+
} from './context';
|
|
7
|
+
import { useSpring, useTrail, useTransition } from './hooks';
|
|
8
|
+
|
|
9
|
+
function itemKey(item: any, index: number): string | number {
|
|
10
|
+
return item?.key ?? item?.id ??
|
|
11
|
+
(typeof item === 'string' || typeof item === 'number' ? item : index);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function keyed(value: unknown, key: string | number): unknown {
|
|
15
|
+
return createElement(Fragment, { key }, value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SpringContext(props: SpringContextValue & { children?: unknown }) @{
|
|
19
|
+
const { children, ...value } = props;
|
|
20
|
+
const inherited = useSpringContext();
|
|
21
|
+
<SpringContextObject value={{ ...inherited, ...value }}>{children}</SpringContextObject>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Spring(props: Record<string, unknown> & { children?: unknown }) @{
|
|
25
|
+
const { children, ...update } = props;
|
|
26
|
+
const styles = useSpring(update);
|
|
27
|
+
<>
|
|
28
|
+
{typeof children === 'function' && !isChildrenBlock(children) ? children(styles) : children}
|
|
29
|
+
</>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Trail(props: any) @{
|
|
33
|
+
const { children, items = [], ...update } = props;
|
|
34
|
+
const styles = useTrail(items.length, update);
|
|
35
|
+
<>
|
|
36
|
+
{typeof children === 'function' && !isChildrenBlock(children)
|
|
37
|
+
? items.map(
|
|
38
|
+
(item: any, index: number) => keyed(
|
|
39
|
+
children(item, index)(styles[index]),
|
|
40
|
+
itemKey(item, index),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
: children}
|
|
44
|
+
</>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Transition(props: any) @{
|
|
48
|
+
const { children, items = [], ...update } = props;
|
|
49
|
+
const transitions = useTransition(items, update);
|
|
50
|
+
<>
|
|
51
|
+
{typeof children === 'function' && !isChildrenBlock(children)
|
|
52
|
+
? transitions(
|
|
53
|
+
(styles: any, item: any, transition: any, index: number) => keyed(
|
|
54
|
+
children(item, index)(styles),
|
|
55
|
+
transition.key,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
: children}
|
|
59
|
+
</>
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SpringContextValue } from './context';
|
|
2
|
+
import type { UseTransitionProps } from './hooks';
|
|
3
|
+
import type { SpringValues } from './types/objects';
|
|
4
|
+
import type { ControllerUpdate, Lookup } from './types/index';
|
|
5
|
+
|
|
6
|
+
export type TrailProps<Item = any, State extends Lookup = Lookup> = ControllerUpdate<State> & {
|
|
7
|
+
items?: Item[];
|
|
8
|
+
children?:
|
|
9
|
+
((item: Item, index: number) => (styles: SpringValues<State> | undefined) => unknown) | unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TransitionProps<Item = any, State extends Lookup = Lookup> = UseTransitionProps<
|
|
13
|
+
Item,
|
|
14
|
+
State
|
|
15
|
+
> & {
|
|
16
|
+
items?: Item | Item[];
|
|
17
|
+
children?: ((item: Item, index: number) => (styles: SpringValues<State>) => unknown) | unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export declare function SpringContext(
|
|
21
|
+
props: SpringContextValue & { children?: unknown },
|
|
22
|
+
scope?: unknown,
|
|
23
|
+
): void;
|
|
24
|
+
|
|
25
|
+
/** Implementation export; public typed overloads live in `spring-component.ts`. */
|
|
26
|
+
export declare function Spring(props: Record<string, unknown> & { children?: unknown }): unknown;
|
|
27
|
+
|
|
28
|
+
export declare function Trail<Item, State extends Lookup = Lookup>(
|
|
29
|
+
props: TrailProps<Item, State>,
|
|
30
|
+
): void;
|
|
31
|
+
export declare function Transition<Item, State extends Lookup = Lookup>(
|
|
32
|
+
props: TransitionProps<Item, State>,
|
|
33
|
+
): void;
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from 'octane';
|
|
2
|
+
import type { ControllerUpdate } from './engine';
|
|
3
|
+
|
|
4
|
+
export type SpringContextValue = Pick<
|
|
5
|
+
ControllerUpdate<Record<string, any>>,
|
|
6
|
+
'cancel' | 'config' | 'immediate' | 'pause'
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export const SpringContext = createContext<SpringContextValue>({});
|
|
10
|
+
|
|
11
|
+
export function useSpringContext(): SpringContextValue {
|
|
12
|
+
return useContext(SpringContext);
|
|
13
|
+
}
|