@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/UPSTREAM.md +103 -0
  4. package/package.json +73 -0
  5. package/src/animated/Animated.ts +24 -0
  6. package/src/animated/AnimatedArray.ts +26 -0
  7. package/src/animated/index.ts +3 -0
  8. package/src/browser.ts +246 -0
  9. package/src/components.tsrx +60 -0
  10. package/src/components.tsrx.d.ts +33 -0
  11. package/src/context.ts +13 -0
  12. package/src/core/AnimationConfig.ts +60 -0
  13. package/src/core/AnimationResult.ts +19 -0
  14. package/src/core/Controller.ts +200 -0
  15. package/src/core/FrameValue.ts +16 -0
  16. package/src/core/Interpolation.ts +46 -0
  17. package/src/core/SpringRef.ts +26 -0
  18. package/src/core/SpringValue.ts +384 -0
  19. package/src/core/index.ts +9 -0
  20. package/src/core/runAsync.ts +17 -0
  21. package/src/core/scheduleProps.ts +34 -0
  22. package/src/core/valueInterpolation.ts +54 -0
  23. package/src/engine.ts +9 -0
  24. package/src/hooks.ts +610 -0
  25. package/src/index.ts +26 -0
  26. package/src/parallax.ts +8 -0
  27. package/src/parallax.tsrx +321 -0
  28. package/src/parallax.tsrx.d.ts +60 -0
  29. package/src/shared/FrameLoop.ts +75 -0
  30. package/src/shared/createInterpolator.ts +85 -0
  31. package/src/shared/globals.ts +7 -0
  32. package/src/shared/index.ts +4 -0
  33. package/src/shared/stringInterpolation.ts +56 -0
  34. package/src/spring-component.ts +36 -0
  35. package/src/types/animated.ts +10 -0
  36. package/src/types/common.ts +29 -0
  37. package/src/types/fluid.ts +9 -0
  38. package/src/types/index.ts +9 -0
  39. package/src/types/interpolate.ts +50 -0
  40. package/src/types/interpolation.ts +18 -0
  41. package/src/types/objects.ts +17 -0
  42. package/src/types/props.ts +275 -0
  43. package/src/types/utils.ts +57 -0
  44. package/src/upstream-compat.ts +164 -0
  45. package/src/web/AnimatedStyle.ts +51 -0
  46. package/src/web/animated.ts +118 -0
  47. package/src/web/applyAnimatedValues.ts +92 -0
  48. 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
+ }
@@ -0,0 +1,3 @@
1
+ // React-free exports ported from @react-spring/animated v10.1.2.
2
+ export * from './Animated';
3
+ export * from './AnimatedArray';
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
+ }