@entrancekit/react 0.1.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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/index.cjs +556 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +551 -0
- package/dist/index.js.map +1 -0
- package/package.json +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 terminalis
|
|
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,110 @@
|
|
|
1
|
+
# @entrancekit/react
|
|
2
|
+
|
|
3
|
+
> Turn an intro animation you already have — a video, a Lottie file, or your own React
|
|
4
|
+
> component — into a **correct** app entrance, without writing the lifecycle yourself.
|
|
5
|
+
|
|
6
|
+
The animation is the easy part. The flow around it is not: playing it on load without
|
|
7
|
+
blocking the app, honoring reduced motion, falling back on slow or failed loads, exiting by
|
|
8
|
+
the model you chose, and tearing down cleanly. **EntranceKit owns that flow. You keep owning
|
|
9
|
+
the animation.**
|
|
10
|
+
|
|
11
|
+
- **Non-blocking** — the overlay never gates your app; it stays interactive underneath.
|
|
12
|
+
- **Reduced-motion aware** — `prefers-reduced-motion` is honored by default (no flash).
|
|
13
|
+
- **Graceful** — skips on error, falls back on timeout, never traps the user.
|
|
14
|
+
- **Self-removing** — deterministic teardown on every exit path, with an optional outro.
|
|
15
|
+
- **Bring your own** — MP4/WebM, Lottie JSON, or any custom React/Motion/GSAP/Canvas/SVG
|
|
16
|
+
component. Client-only, single package, **zero runtime dependencies**, SSR-safe.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @entrancekit/react
|
|
22
|
+
# Lottie support is optional and lazy-loaded:
|
|
23
|
+
npm install @lottiefiles/dotlottie-react
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Drop in a video
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { EntranceVideo } from '@entrancekit/react';
|
|
30
|
+
|
|
31
|
+
export default function Root() {
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<App /> {/* renders and stays interactive the whole time */}
|
|
35
|
+
<EntranceVideo
|
|
36
|
+
id="launch"
|
|
37
|
+
sources={[
|
|
38
|
+
{ src: '/intro.webm', type: 'video/webm' },
|
|
39
|
+
{ src: '/intro.mp4', type: 'video/mp4' },
|
|
40
|
+
]}
|
|
41
|
+
poster="/intro-poster.png"
|
|
42
|
+
fallbackAfter={2500}
|
|
43
|
+
/>
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Bring your own component (the core)
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { EntranceController } from '@entrancekit/react';
|
|
53
|
+
|
|
54
|
+
<EntranceController id="launch">
|
|
55
|
+
{({ shouldAnimate, complete, skip }) => (
|
|
56
|
+
<MyIntro
|
|
57
|
+
play={shouldAnimate} // start when told
|
|
58
|
+
onComplete={complete} // its OWN done-signal — the preferred path
|
|
59
|
+
onError={skip} // never trap the user
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
</EntranceController>;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## The lifecycle
|
|
66
|
+
|
|
67
|
+
EntranceKit owns one canonical state machine:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
idle → preload → play → resolve → done
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Every way an entrance can end — the animation's own done-signal, a user skip, a timeout, an
|
|
74
|
+
error, or a reduced-motion suppression — funnels through a single `resolve` carrying a
|
|
75
|
+
`reason`, so teardown is deterministic on every path. Wire your animation's native end event
|
|
76
|
+
to `complete()`; `fallbackAfter` is only a safety net for a hung or slow load, never a cap on
|
|
77
|
+
a correctly-signalling animation.
|
|
78
|
+
|
|
79
|
+
## API at a glance
|
|
80
|
+
|
|
81
|
+
| Export | What it is |
|
|
82
|
+
| -------------------- | -------------------------------------------------------------------- |
|
|
83
|
+
| `EntranceController` | Render-prop for a bring-your-own animation. The core. |
|
|
84
|
+
| `EntranceVideo` | MP4/WebM convenience entry point (prefers native `ended`). |
|
|
85
|
+
| `EntranceLottie` | Lottie JSON entry point (optional, lazy-loaded player). |
|
|
86
|
+
| `useEntrance` | The same engine, headless, for a fully custom overlay. |
|
|
87
|
+
|
|
88
|
+
Shared props: `id` (required), `fallbackAfter` (default `4000`), `reducedMotion`
|
|
89
|
+
(`'skip'` \| `'play'`, default `'skip'`), `skipControl` (`true` \| render-fn \| `false`),
|
|
90
|
+
`outro` (`number` \| `{ durationMs, render }`), `container`, and
|
|
91
|
+
`onResolve` / `onError` / `onStateChange`.
|
|
92
|
+
|
|
93
|
+
> There is no `mode`, no persistence, and no show-policy in v0.1.0 — an entrance is a
|
|
94
|
+
> transient overlay that plays and leaves, not a once-per-user gate.
|
|
95
|
+
|
|
96
|
+
## Demo
|
|
97
|
+
|
|
98
|
+
A live Playground (every source, every scenario, every exit model) is at
|
|
99
|
+
**[terminalis.github.io/entrancekit](https://terminalis.github.io/entrancekit/)** (deployed by
|
|
100
|
+
CI from [`examples/demo`](./examples/demo)). Run it locally:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npm install && npm run build
|
|
104
|
+
npm --prefix examples/demo install
|
|
105
|
+
npm --prefix examples/demo run dev
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var reactDom = require('react-dom');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/useEntrance.ts
|
|
9
|
+
|
|
10
|
+
// src/lifecycle/reducer.ts
|
|
11
|
+
var ACTIVE_PHASES = ["idle", "preload", "play"];
|
|
12
|
+
var GRACEFUL_REASONS = ["completed", "skipped", "timeout"];
|
|
13
|
+
var isActive = (phase) => ACTIVE_PHASES.includes(phase);
|
|
14
|
+
function makeResolve(reason, error = null) {
|
|
15
|
+
return { phase: "resolve", reason, error };
|
|
16
|
+
}
|
|
17
|
+
var initialMachineState = {
|
|
18
|
+
phase: "idle",
|
|
19
|
+
reason: null,
|
|
20
|
+
error: null
|
|
21
|
+
};
|
|
22
|
+
function shouldRunOutro(reason, hasOutro) {
|
|
23
|
+
return hasOutro && GRACEFUL_REASONS.includes(reason);
|
|
24
|
+
}
|
|
25
|
+
function entranceReducer(state, action) {
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case "BEGIN":
|
|
28
|
+
return state.phase === "idle" ? { ...state, phase: "preload" } : state;
|
|
29
|
+
case "PRELOADED":
|
|
30
|
+
return state.phase === "preload" ? { ...state, phase: "play" } : state;
|
|
31
|
+
case "COMPLETE":
|
|
32
|
+
return state.phase === "preload" || state.phase === "play" ? makeResolve("completed") : state;
|
|
33
|
+
case "SKIP":
|
|
34
|
+
return state.phase === "preload" || state.phase === "play" ? makeResolve("skipped") : state;
|
|
35
|
+
case "TIMEOUT":
|
|
36
|
+
return state.phase === "preload" || state.phase === "play" ? makeResolve("timeout") : state;
|
|
37
|
+
case "FAIL":
|
|
38
|
+
return state.phase === "preload" || state.phase === "play" ? makeResolve("error", {
|
|
39
|
+
kind: action.kind ?? "unknown",
|
|
40
|
+
cause: action.cause
|
|
41
|
+
}) : state;
|
|
42
|
+
case "REDUCED_MOTION":
|
|
43
|
+
return isActive(state.phase) ? makeResolve("reduced-motion") : state;
|
|
44
|
+
case "ENTER_OUTRO":
|
|
45
|
+
return state.phase === "resolve" ? { ...state, phase: "outro" } : state;
|
|
46
|
+
case "ENTER_DONE":
|
|
47
|
+
return state.phase === "resolve" || state.phase === "outro" ? { ...state, phase: "done" } : state;
|
|
48
|
+
default: {
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/lifecycle/reducedMotion.ts
|
|
55
|
+
var QUERY = "(prefers-reduced-motion: reduce)";
|
|
56
|
+
function getMediaQueryList() {
|
|
57
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return window.matchMedia(QUERY);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function prefersReducedMotion() {
|
|
67
|
+
return getMediaQueryList()?.matches ?? false;
|
|
68
|
+
}
|
|
69
|
+
function subscribeReducedMotion(onChange) {
|
|
70
|
+
const mql = getMediaQueryList();
|
|
71
|
+
if (!mql) return () => {
|
|
72
|
+
};
|
|
73
|
+
const handler = (event) => onChange(event.matches);
|
|
74
|
+
if (typeof mql.addEventListener === "function") {
|
|
75
|
+
mql.addEventListener("change", handler);
|
|
76
|
+
return () => mql.removeEventListener("change", handler);
|
|
77
|
+
}
|
|
78
|
+
const legacy = mql;
|
|
79
|
+
legacy.addListener?.(handler);
|
|
80
|
+
return () => legacy.removeListener?.(handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/lifecycle/useEntranceMachine.ts
|
|
84
|
+
function useEntranceMachine(config) {
|
|
85
|
+
const [state, dispatch] = react.useReducer(entranceReducer, initialMachineState);
|
|
86
|
+
const { phase, reason } = state;
|
|
87
|
+
const { reducedMotion, fallbackAfter, preloadTimeout, outroTimeout, hasOutro } = config;
|
|
88
|
+
const actions = react.useMemo(
|
|
89
|
+
() => ({
|
|
90
|
+
preloaded: () => dispatch({ type: "PRELOADED" }),
|
|
91
|
+
complete: () => dispatch({ type: "COMPLETE" }),
|
|
92
|
+
skip: () => dispatch({ type: "SKIP" }),
|
|
93
|
+
fail: (kind, cause) => dispatch({ type: "FAIL", kind, cause }),
|
|
94
|
+
outroComplete: () => dispatch({ type: "ENTER_DONE" })
|
|
95
|
+
}),
|
|
96
|
+
[]
|
|
97
|
+
);
|
|
98
|
+
react.useEffect(() => {
|
|
99
|
+
if (reducedMotion === "skip" && prefersReducedMotion()) {
|
|
100
|
+
dispatch({ type: "REDUCED_MOTION" });
|
|
101
|
+
} else {
|
|
102
|
+
dispatch({ type: "BEGIN" });
|
|
103
|
+
}
|
|
104
|
+
if (reducedMotion !== "skip") return;
|
|
105
|
+
return subscribeReducedMotion((reduced) => {
|
|
106
|
+
if (reduced) dispatch({ type: "REDUCED_MOTION" });
|
|
107
|
+
});
|
|
108
|
+
}, [reducedMotion]);
|
|
109
|
+
react.useEffect(() => {
|
|
110
|
+
let id;
|
|
111
|
+
if (phase === "preload") {
|
|
112
|
+
id = setTimeout(() => dispatch({ type: "TIMEOUT" }), preloadTimeout);
|
|
113
|
+
} else if (phase === "play") {
|
|
114
|
+
id = setTimeout(() => dispatch({ type: "TIMEOUT" }), fallbackAfter);
|
|
115
|
+
} else if (phase === "outro") {
|
|
116
|
+
id = setTimeout(() => dispatch({ type: "ENTER_DONE" }), outroTimeout);
|
|
117
|
+
}
|
|
118
|
+
return () => {
|
|
119
|
+
if (id !== void 0) clearTimeout(id);
|
|
120
|
+
};
|
|
121
|
+
}, [phase, preloadTimeout, fallbackAfter, outroTimeout]);
|
|
122
|
+
react.useEffect(() => {
|
|
123
|
+
if (phase !== "resolve" || reason === null) return;
|
|
124
|
+
dispatch(
|
|
125
|
+
shouldRunOutro(reason, hasOutro) ? { type: "ENTER_OUTRO" } : { type: "ENTER_DONE" }
|
|
126
|
+
);
|
|
127
|
+
}, [phase, reason, hasOutro]);
|
|
128
|
+
return { state, actions };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/lifecycle/defaults.ts
|
|
132
|
+
var DEFAULT_FALLBACK_AFTER = 4e3;
|
|
133
|
+
var DEFAULT_OUTRO_TIMEOUT = 1500;
|
|
134
|
+
var DEFAULT_REDUCED_MOTION = "skip";
|
|
135
|
+
function resolveConfig(input = {}) {
|
|
136
|
+
const fallbackAfter = input.fallbackAfter ?? DEFAULT_FALLBACK_AFTER;
|
|
137
|
+
return {
|
|
138
|
+
fallbackAfter,
|
|
139
|
+
preloadTimeout: input.preloadTimeout ?? fallbackAfter,
|
|
140
|
+
outroTimeout: input.outroTimeout ?? DEFAULT_OUTRO_TIMEOUT,
|
|
141
|
+
reducedMotion: input.reducedMotion ?? DEFAULT_REDUCED_MOTION,
|
|
142
|
+
hasOutro: input.hasOutro ?? false
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/internal/config.ts
|
|
147
|
+
function normalizeOutro(outro) {
|
|
148
|
+
if (outro == null) return { hasOutro: false, durationMs: DEFAULT_OUTRO_TIMEOUT };
|
|
149
|
+
if (typeof outro === "number") return { hasOutro: true, durationMs: outro };
|
|
150
|
+
return { hasOutro: true, durationMs: outro.durationMs, render: outro.render };
|
|
151
|
+
}
|
|
152
|
+
function configFromOptions(input) {
|
|
153
|
+
const outro = normalizeOutro(input.outro);
|
|
154
|
+
return resolveConfig({
|
|
155
|
+
fallbackAfter: input.fallbackAfter,
|
|
156
|
+
reducedMotion: input.reducedMotion,
|
|
157
|
+
hasOutro: outro.hasOutro,
|
|
158
|
+
outroTimeout: outro.durationMs
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/internal/deriveRenderProps.ts
|
|
163
|
+
function deriveRenderProps(state, actions) {
|
|
164
|
+
const { phase, reason } = state;
|
|
165
|
+
return {
|
|
166
|
+
// The transient `outro` phase is surfaced publicly as `'resolve'`.
|
|
167
|
+
state: phase === "outro" ? "resolve" : phase,
|
|
168
|
+
reason,
|
|
169
|
+
shouldAnimate: phase === "play",
|
|
170
|
+
shouldPlayOutro: phase === "outro",
|
|
171
|
+
isOverlayMounted: phase !== "done",
|
|
172
|
+
isPlaying: phase === "play",
|
|
173
|
+
isDone: phase === "done",
|
|
174
|
+
complete: actions.complete,
|
|
175
|
+
skip: actions.skip,
|
|
176
|
+
// Public error path: kind defaults to 'unknown'; the wrappers classify their
|
|
177
|
+
// own errors (load vs playback) via the raw machine action instead.
|
|
178
|
+
fail: (error) => actions.fail(void 0, error),
|
|
179
|
+
outroComplete: actions.outroComplete
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function useEntranceCallbacks(id, state, { onResolve, onError, onStateChange }) {
|
|
183
|
+
const resolveFired = react.useRef(false);
|
|
184
|
+
react.useEffect(() => {
|
|
185
|
+
if (state.reason === null || resolveFired.current) return;
|
|
186
|
+
resolveFired.current = true;
|
|
187
|
+
onResolve?.({ id, reason: state.reason });
|
|
188
|
+
if (state.reason === "error") {
|
|
189
|
+
onError?.({ id, kind: state.error?.kind ?? "unknown", cause: state.error?.cause });
|
|
190
|
+
}
|
|
191
|
+
}, [state.reason, state.error, id, onResolve, onError]);
|
|
192
|
+
const publicState = state.phase === "outro" ? "resolve" : state.phase;
|
|
193
|
+
const lastState = react.useRef(null);
|
|
194
|
+
react.useEffect(() => {
|
|
195
|
+
if (lastState.current === publicState) return;
|
|
196
|
+
lastState.current = publicState;
|
|
197
|
+
onStateChange?.(publicState);
|
|
198
|
+
}, [publicState, onStateChange]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/useEntrance.ts
|
|
202
|
+
function useEntrance(options) {
|
|
203
|
+
const { id, fallbackAfter, reducedMotion, outro, onResolve, onError, onStateChange } = options;
|
|
204
|
+
const config = configFromOptions({ fallbackAfter, reducedMotion, outro });
|
|
205
|
+
const { state, actions } = useEntranceMachine(config);
|
|
206
|
+
react.useEffect(() => {
|
|
207
|
+
actions.preloaded();
|
|
208
|
+
}, [actions]);
|
|
209
|
+
useEntranceCallbacks(id, state, { onResolve, onError, onStateChange });
|
|
210
|
+
return deriveRenderProps(state, actions);
|
|
211
|
+
}
|
|
212
|
+
var emptySubscribe = () => () => {
|
|
213
|
+
};
|
|
214
|
+
function useIsClient() {
|
|
215
|
+
return react.useSyncExternalStore(
|
|
216
|
+
emptySubscribe,
|
|
217
|
+
() => true,
|
|
218
|
+
// client snapshot
|
|
219
|
+
() => false
|
|
220
|
+
// server / first-render snapshot
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function useOverlayHost({
|
|
224
|
+
enabled,
|
|
225
|
+
container,
|
|
226
|
+
zIndex
|
|
227
|
+
}) {
|
|
228
|
+
const [host, setHost] = react.useState(null);
|
|
229
|
+
react.useEffect(() => {
|
|
230
|
+
if (!enabled) {
|
|
231
|
+
setHost(null);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (container) {
|
|
235
|
+
setHost(container);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const el = document.createElement("div");
|
|
239
|
+
el.setAttribute("data-entrancekit-host", "");
|
|
240
|
+
el.style.position = "fixed";
|
|
241
|
+
el.style.inset = "0";
|
|
242
|
+
el.style.pointerEvents = "none";
|
|
243
|
+
el.style.zIndex = zIndex !== void 0 ? String(zIndex) : "var(--entrancekit-z, 2147483647)";
|
|
244
|
+
document.body.appendChild(el);
|
|
245
|
+
setHost(el);
|
|
246
|
+
return () => {
|
|
247
|
+
el.remove();
|
|
248
|
+
setHost(null);
|
|
249
|
+
};
|
|
250
|
+
}, [enabled, container, zIndex]);
|
|
251
|
+
return host;
|
|
252
|
+
}
|
|
253
|
+
var DEFAULT_SKIP_LABEL = "Skip intro";
|
|
254
|
+
var defaultSkipStyle = {
|
|
255
|
+
position: "absolute",
|
|
256
|
+
top: 16,
|
|
257
|
+
right: 16,
|
|
258
|
+
pointerEvents: "auto",
|
|
259
|
+
padding: "6px 12px",
|
|
260
|
+
font: "inherit",
|
|
261
|
+
fontSize: 14,
|
|
262
|
+
lineHeight: 1.2,
|
|
263
|
+
color: "#fff",
|
|
264
|
+
background: "rgba(0, 0, 0, 0.55)",
|
|
265
|
+
border: "1px solid rgba(255, 255, 255, 0.35)",
|
|
266
|
+
borderRadius: 6,
|
|
267
|
+
cursor: "pointer"
|
|
268
|
+
};
|
|
269
|
+
function EntranceOverlay({
|
|
270
|
+
api,
|
|
271
|
+
children,
|
|
272
|
+
outro,
|
|
273
|
+
skipControl = true,
|
|
274
|
+
skipLabel = DEFAULT_SKIP_LABEL,
|
|
275
|
+
zIndex,
|
|
276
|
+
container,
|
|
277
|
+
className,
|
|
278
|
+
style
|
|
279
|
+
}) {
|
|
280
|
+
const isClient = useIsClient();
|
|
281
|
+
const host = useOverlayHost({
|
|
282
|
+
enabled: isClient && api.isOverlayMounted,
|
|
283
|
+
container,
|
|
284
|
+
zIndex
|
|
285
|
+
});
|
|
286
|
+
const previouslyFocused = react.useRef(null);
|
|
287
|
+
react.useEffect(() => {
|
|
288
|
+
previouslyFocused.current = document.activeElement;
|
|
289
|
+
return () => {
|
|
290
|
+
const prev = previouslyFocused.current;
|
|
291
|
+
const active = document.activeElement;
|
|
292
|
+
if (prev instanceof HTMLElement && active instanceof HTMLElement && active.closest("[data-entrancekit-overlay]")) {
|
|
293
|
+
prev.focus();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}, []);
|
|
297
|
+
if (!isClient || !host || !api.isOverlayMounted) return null;
|
|
298
|
+
const showVisuals = api.state !== "idle" && api.reason !== "reduced-motion";
|
|
299
|
+
const showSkip = skipControl !== false && (api.state === "preload" || api.state === "play");
|
|
300
|
+
const stageStyle = {
|
|
301
|
+
position: "absolute",
|
|
302
|
+
inset: 0,
|
|
303
|
+
pointerEvents: "none",
|
|
304
|
+
opacity: api.shouldPlayOutro ? 0 : 1,
|
|
305
|
+
transition: api.shouldPlayOutro ? `opacity ${outro.durationMs}ms ease` : void 0,
|
|
306
|
+
...style
|
|
307
|
+
};
|
|
308
|
+
const visuals = api.shouldPlayOutro && outro.render && api.reason ? outro.render({ reason: api.reason }) : children;
|
|
309
|
+
return reactDom.createPortal(
|
|
310
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { "data-entrancekit-overlay": "", className, style: stageStyle, children: [
|
|
311
|
+
showVisuals && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", inset: 0 }, children: visuals }),
|
|
312
|
+
showSkip && (typeof skipControl === "function" ? skipControl(api.skip) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
313
|
+
"button",
|
|
314
|
+
{
|
|
315
|
+
type: "button",
|
|
316
|
+
onClick: api.skip,
|
|
317
|
+
"aria-label": skipLabel,
|
|
318
|
+
"data-entrancekit-skip": "",
|
|
319
|
+
style: defaultSkipStyle,
|
|
320
|
+
children: skipLabel
|
|
321
|
+
}
|
|
322
|
+
))
|
|
323
|
+
] }),
|
|
324
|
+
host
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
function EntranceController(props) {
|
|
328
|
+
const {
|
|
329
|
+
children,
|
|
330
|
+
skipControl,
|
|
331
|
+
skipLabel,
|
|
332
|
+
zIndex,
|
|
333
|
+
container,
|
|
334
|
+
className,
|
|
335
|
+
style,
|
|
336
|
+
...lifecycle
|
|
337
|
+
} = props;
|
|
338
|
+
const api = useEntrance(lifecycle);
|
|
339
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
340
|
+
EntranceOverlay,
|
|
341
|
+
{
|
|
342
|
+
api,
|
|
343
|
+
outro: normalizeOutro(props.outro),
|
|
344
|
+
skipControl,
|
|
345
|
+
skipLabel,
|
|
346
|
+
zIndex,
|
|
347
|
+
container,
|
|
348
|
+
className,
|
|
349
|
+
style,
|
|
350
|
+
children: children(api)
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/internal/useEntranceEngine.ts
|
|
356
|
+
function useEntranceEngine(options) {
|
|
357
|
+
const config = configFromOptions(options);
|
|
358
|
+
const { state, actions } = useEntranceMachine(config);
|
|
359
|
+
useEntranceCallbacks(options.id, state, options);
|
|
360
|
+
return { state, actions, api: deriveRenderProps(state, actions) };
|
|
361
|
+
}
|
|
362
|
+
var videoStyle = {
|
|
363
|
+
position: "absolute",
|
|
364
|
+
inset: 0,
|
|
365
|
+
width: "100%",
|
|
366
|
+
height: "100%",
|
|
367
|
+
objectFit: "cover",
|
|
368
|
+
display: "block"
|
|
369
|
+
};
|
|
370
|
+
function EntranceVideo(props) {
|
|
371
|
+
const {
|
|
372
|
+
id,
|
|
373
|
+
src,
|
|
374
|
+
sources,
|
|
375
|
+
poster,
|
|
376
|
+
muted = true,
|
|
377
|
+
playsInline = true,
|
|
378
|
+
preload = "auto",
|
|
379
|
+
fallbackAfter,
|
|
380
|
+
reducedMotion,
|
|
381
|
+
outro,
|
|
382
|
+
skipControl,
|
|
383
|
+
skipLabel,
|
|
384
|
+
zIndex,
|
|
385
|
+
container,
|
|
386
|
+
className,
|
|
387
|
+
style,
|
|
388
|
+
onResolve,
|
|
389
|
+
onError,
|
|
390
|
+
onStateChange
|
|
391
|
+
} = props;
|
|
392
|
+
const { state, actions, api } = useEntranceEngine({
|
|
393
|
+
id,
|
|
394
|
+
fallbackAfter,
|
|
395
|
+
reducedMotion,
|
|
396
|
+
outro,
|
|
397
|
+
onResolve,
|
|
398
|
+
onError,
|
|
399
|
+
onStateChange
|
|
400
|
+
});
|
|
401
|
+
const videoRef = react.useRef(null);
|
|
402
|
+
const endedFired = react.useRef(false);
|
|
403
|
+
react.useEffect(() => {
|
|
404
|
+
if (state.phase !== "play") return;
|
|
405
|
+
const video = videoRef.current;
|
|
406
|
+
if (!video) return;
|
|
407
|
+
try {
|
|
408
|
+
video.muted = muted;
|
|
409
|
+
const result = video.play();
|
|
410
|
+
if (result && typeof result.then === "function") {
|
|
411
|
+
result.catch((err) => actions.fail("playback", err));
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
actions.fail("playback", err);
|
|
415
|
+
}
|
|
416
|
+
}, [state.phase, actions, muted]);
|
|
417
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
418
|
+
EntranceOverlay,
|
|
419
|
+
{
|
|
420
|
+
api,
|
|
421
|
+
outro: normalizeOutro(outro),
|
|
422
|
+
skipControl,
|
|
423
|
+
skipLabel,
|
|
424
|
+
zIndex,
|
|
425
|
+
container,
|
|
426
|
+
className,
|
|
427
|
+
style,
|
|
428
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
429
|
+
"video",
|
|
430
|
+
{
|
|
431
|
+
ref: videoRef,
|
|
432
|
+
...src ? { src } : {},
|
|
433
|
+
poster,
|
|
434
|
+
muted,
|
|
435
|
+
playsInline,
|
|
436
|
+
preload,
|
|
437
|
+
onCanPlayThrough: () => actions.preloaded(),
|
|
438
|
+
onLoadedData: () => actions.preloaded(),
|
|
439
|
+
onEnded: () => {
|
|
440
|
+
if (endedFired.current) return;
|
|
441
|
+
endedFired.current = true;
|
|
442
|
+
actions.complete();
|
|
443
|
+
},
|
|
444
|
+
onError: () => actions.fail("load"),
|
|
445
|
+
style: videoStyle,
|
|
446
|
+
children: sources?.map((source) => /* @__PURE__ */ jsxRuntime.jsx("source", { src: source.src, type: source.type }, source.src))
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
var PEER = "@lottiefiles/dotlottie-react";
|
|
453
|
+
var playerStyle = {
|
|
454
|
+
position: "absolute",
|
|
455
|
+
inset: 0,
|
|
456
|
+
width: "100%",
|
|
457
|
+
height: "100%",
|
|
458
|
+
display: "block"
|
|
459
|
+
};
|
|
460
|
+
function EntranceLottie(props) {
|
|
461
|
+
const {
|
|
462
|
+
id,
|
|
463
|
+
src,
|
|
464
|
+
animationData,
|
|
465
|
+
onComplete,
|
|
466
|
+
fallbackAfter,
|
|
467
|
+
reducedMotion,
|
|
468
|
+
outro,
|
|
469
|
+
skipControl,
|
|
470
|
+
skipLabel,
|
|
471
|
+
zIndex,
|
|
472
|
+
container,
|
|
473
|
+
className,
|
|
474
|
+
style,
|
|
475
|
+
onResolve,
|
|
476
|
+
onError,
|
|
477
|
+
onStateChange
|
|
478
|
+
} = props;
|
|
479
|
+
const { state, actions, api } = useEntranceEngine({
|
|
480
|
+
id,
|
|
481
|
+
fallbackAfter,
|
|
482
|
+
reducedMotion,
|
|
483
|
+
outro,
|
|
484
|
+
onResolve,
|
|
485
|
+
onError,
|
|
486
|
+
onStateChange
|
|
487
|
+
});
|
|
488
|
+
const [Player, setPlayer] = react.useState(null);
|
|
489
|
+
const shouldLoad = state.phase === "preload" || state.phase === "play";
|
|
490
|
+
react.useEffect(() => {
|
|
491
|
+
if (!shouldLoad) return;
|
|
492
|
+
let alive = true;
|
|
493
|
+
const reportMissingPeer = (err) => {
|
|
494
|
+
if (typeof console !== "undefined") {
|
|
495
|
+
console.error(
|
|
496
|
+
`[EntranceKit] EntranceLottie requires the optional peer "${PEER}". Install it with: npm i ${PEER}`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
actions.fail("missing-peer", err);
|
|
500
|
+
};
|
|
501
|
+
import('@lottiefiles/dotlottie-react').then((mod) => {
|
|
502
|
+
if (!alive) return;
|
|
503
|
+
const Comp = mod.DotLottieReact;
|
|
504
|
+
if (Comp) setPlayer(() => Comp);
|
|
505
|
+
else reportMissingPeer(new Error(`${PEER} did not export DotLottieReact`));
|
|
506
|
+
}).catch((err) => {
|
|
507
|
+
if (alive) reportMissingPeer(err);
|
|
508
|
+
});
|
|
509
|
+
return () => {
|
|
510
|
+
alive = false;
|
|
511
|
+
};
|
|
512
|
+
}, [shouldLoad, actions]);
|
|
513
|
+
const handleRef = react.useCallback(
|
|
514
|
+
(instance) => {
|
|
515
|
+
if (!instance) return;
|
|
516
|
+
instance.addEventListener("load", () => actions.preloaded());
|
|
517
|
+
instance.addEventListener("complete", () => {
|
|
518
|
+
actions.complete();
|
|
519
|
+
onComplete?.();
|
|
520
|
+
});
|
|
521
|
+
instance.addEventListener("loadError", () => actions.fail("load"));
|
|
522
|
+
},
|
|
523
|
+
[actions, onComplete]
|
|
524
|
+
);
|
|
525
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
526
|
+
EntranceOverlay,
|
|
527
|
+
{
|
|
528
|
+
api,
|
|
529
|
+
outro: normalizeOutro(outro),
|
|
530
|
+
skipControl,
|
|
531
|
+
skipLabel,
|
|
532
|
+
zIndex,
|
|
533
|
+
container,
|
|
534
|
+
className,
|
|
535
|
+
style,
|
|
536
|
+
children: Player ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
537
|
+
Player,
|
|
538
|
+
{
|
|
539
|
+
...src ? { src } : {},
|
|
540
|
+
data: animationData,
|
|
541
|
+
loop: false,
|
|
542
|
+
autoplay: true,
|
|
543
|
+
dotLottieRefCallback: handleRef,
|
|
544
|
+
style: playerStyle
|
|
545
|
+
}
|
|
546
|
+
) : null
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
exports.EntranceController = EntranceController;
|
|
552
|
+
exports.EntranceLottie = EntranceLottie;
|
|
553
|
+
exports.EntranceVideo = EntranceVideo;
|
|
554
|
+
exports.useEntrance = useEntrance;
|
|
555
|
+
//# sourceMappingURL=index.cjs.map
|
|
556
|
+
//# sourceMappingURL=index.cjs.map
|