apex-ui-kit 2.5.0 → 2.6.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
CHANGED
|
@@ -240,11 +240,11 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
240
240
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
241
241
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
242
242
|
|
|
243
|
-
## What's new in 2.
|
|
243
|
+
## What's new in 2.6
|
|
244
244
|
|
|
245
245
|
- 27 new components, including WebGL and three.js (Galaxy 3D, Warp Tunnel, Distortion Image, Liquid Blob 3D, Particle Morph, Retro Grid 3D), the AI voice family (Wave, Bars, Ring, Blob), Gooey Nav (WebGL metaball pill), Coverflow 3D, Card Swap 3D, 3D Text Ring, Elastic Slider, Stepper and Pixel Transition.
|
|
246
246
|
- AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
|
|
247
|
-
- Loading Screens
|
|
247
|
+
- Loading Screens follow the real page load by default: drop `<LoadingScreen fullScreen />` into your app and it covers the page while it loads, with a live bar and percentage, then hides itself. Pass `progress` to drive it yourself.
|
|
248
248
|
- Hold to Confirm follows a delete flow with an `UndoNotice` that counts down before the action is final.
|
|
249
249
|
- Sliders and drag components no longer stop midway when the page text gets selected.
|
|
250
250
|
- Stepper and Animated Tabs keep a steady height, Shiny Text loops seamlessly, Cube Gallery is a real 3D cube and Flip Book is a hardcover book you can drag.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { AnimatePresence, motion, useAnimationControls, useReducedMotion } from "framer-motion";
|
|
3
|
+
import { usePageProgress } from "./usePageProgress.js";
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
* Loading Screen: five ways to wait.
|
|
@@ -9,50 +10,27 @@ import { AnimatePresence, motion, useAnimationControls, useReducedMotion } from
|
|
|
9
10
|
* catch – a mini game: tap the orb before its ring runs out and build a combo
|
|
10
11
|
* runner – a mini game: jump the neon pillars (Space / ↑ / tap) while things load
|
|
11
12
|
* racer – a mini game: steer through traffic (← → / tap a side) while things load
|
|
12
|
-
*
|
|
13
|
+
* By default it shows the real page load: the bar and the count follow the
|
|
14
|
+
* images, scripts, styles and fonts the page is actually loading, and it
|
|
15
|
+
* hides itself once everything has loaded (autoHide). Pass `progress`
|
|
16
|
+
* (0 to 100) to drive it yourself instead.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
const MESSAGES = ["
|
|
19
|
+
const MESSAGES = ["Starting up", "Loading page", "Loading content", "Almost ready"];
|
|
16
20
|
const messageFor = (v) => MESSAGES[Math.min(MESSAGES.length - 1, Math.floor(v / (100 / MESSAGES.length)))];
|
|
17
21
|
|
|
18
|
-
//
|
|
19
|
-
function useProgress(progress,
|
|
20
|
-
const
|
|
22
|
+
// Real progress: your own `progress` when given, otherwise the page's real load.
|
|
23
|
+
function useProgress(progress, onComplete) {
|
|
24
|
+
const controlled = typeof progress === "number";
|
|
25
|
+
const page = usePageProgress(!controlled);
|
|
26
|
+
const value = Math.round(Math.max(0, Math.min(100, controlled ? progress : page.progress)));
|
|
21
27
|
const doneRef = useRef(onComplete);
|
|
22
28
|
doneRef.current = onComplete;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (controlled) return undefined;
|
|
27
|
-
let raf = 0;
|
|
28
|
-
let start = performance.now();
|
|
29
|
-
let fired = false;
|
|
30
|
-
const tick = (now) => {
|
|
31
|
-
const t = Math.min(1, Math.max(0, (now - start) / duration));
|
|
32
|
-
// Fast start, slow middle, quick finish — the way real loads feel.
|
|
33
|
-
const eased = t < 0.85 ? 0.92 * (1 - Math.pow(1 - t / 0.85, 2.2)) : 0.92 + 0.08 * ((t - 0.85) / 0.15);
|
|
34
|
-
setAuto(Math.round(eased * 100));
|
|
35
|
-
if (t >= 1) {
|
|
36
|
-
if (!fired) {
|
|
37
|
-
fired = true;
|
|
38
|
-
doneRef.current?.();
|
|
39
|
-
}
|
|
40
|
-
if (loop) {
|
|
41
|
-
start = now + 900;
|
|
42
|
-
fired = false;
|
|
43
|
-
} else return;
|
|
44
|
-
}
|
|
45
|
-
raf = requestAnimationFrame(tick);
|
|
46
|
-
};
|
|
47
|
-
raf = requestAnimationFrame(tick);
|
|
48
|
-
return () => cancelAnimationFrame(raf);
|
|
49
|
-
}, [controlled, duration, loop]);
|
|
50
|
-
|
|
29
|
+
const complete = value >= 100 && (controlled || page.done);
|
|
51
30
|
useEffect(() => {
|
|
52
|
-
if (
|
|
53
|
-
}, [
|
|
54
|
-
|
|
55
|
-
return Math.round(Math.max(0, Math.min(100, controlled ? progress : auto)));
|
|
31
|
+
if (complete) doneRef.current?.();
|
|
32
|
+
}, [complete]);
|
|
33
|
+
return { value, complete };
|
|
56
34
|
}
|
|
57
35
|
|
|
58
36
|
function Bar({ value, color, track }) {
|
|
@@ -108,8 +86,8 @@ function Orbit({ value, color, moving }) {
|
|
|
108
86
|
);
|
|
109
87
|
}
|
|
110
88
|
|
|
111
|
-
function ProgressView({ value, color, track, moving }) {
|
|
112
|
-
const message = messageFor(value)
|
|
89
|
+
function ProgressView({ value, color, track, moving, statusText }) {
|
|
90
|
+
const message = statusText ?? `${messageFor(value)}…`;
|
|
113
91
|
return (
|
|
114
92
|
<div className="w-[min(22rem,80vw)]">
|
|
115
93
|
<p className="font-mono text-[clamp(3.5rem,12vw,5.5rem)] font-bold leading-none tabular-nums tracking-tight">
|
|
@@ -122,7 +100,7 @@ function ProgressView({ value, color, track, moving }) {
|
|
|
122
100
|
<div className="mt-3 h-5 overflow-hidden text-sm opacity-70">
|
|
123
101
|
<AnimatePresence mode="wait" initial={false}>
|
|
124
102
|
<motion.p key={message} initial={{ y: 14, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -14, opacity: 0 }} transition={{ duration: moving ? 0.25 : 0 }}>
|
|
125
|
-
{message}
|
|
103
|
+
{message}
|
|
126
104
|
</motion.p>
|
|
127
105
|
</AnimatePresence>
|
|
128
106
|
</div>
|
|
@@ -760,30 +738,39 @@ function RacerGame({ value, color, track, dark, statusText }) {
|
|
|
760
738
|
const LoadingScreen = ({
|
|
761
739
|
variant = "orbit",
|
|
762
740
|
progress,
|
|
763
|
-
duration = 6000,
|
|
764
|
-
loop = true,
|
|
765
741
|
accentColor = "#a3e635",
|
|
766
742
|
theme = "dark",
|
|
767
743
|
title = "Loading",
|
|
768
744
|
fullScreen = false,
|
|
745
|
+
autoHide = true,
|
|
769
746
|
onComplete,
|
|
770
747
|
statusText,
|
|
771
748
|
className = "",
|
|
772
749
|
}) => {
|
|
773
750
|
const moving = !useReducedMotion();
|
|
774
|
-
const value = useProgress(progress,
|
|
751
|
+
const { value, complete } = useProgress(progress, onComplete);
|
|
752
|
+
const [gone, setGone] = useState(false);
|
|
775
753
|
const dark = theme !== "light";
|
|
776
754
|
const track = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
|
|
777
|
-
|
|
755
|
+
// Page-style status ("Loading page…", "Almost ready…"), or your own text.
|
|
756
|
+
const status = statusText ?? (complete ? "Page ready" : undefined);
|
|
757
|
+
const shared = { value, color: accentColor, track, moving, dark, statusText: status };
|
|
778
758
|
const game = variant === "catch" || variant === "runner" || variant === "racer";
|
|
759
|
+
const hiding = autoHide && complete;
|
|
779
760
|
|
|
761
|
+
if (gone) return null;
|
|
780
762
|
return (
|
|
781
|
-
<div
|
|
763
|
+
<motion.div
|
|
782
764
|
data-loading-screen=""
|
|
783
765
|
role="status"
|
|
784
766
|
aria-live="polite"
|
|
767
|
+
aria-busy={!complete}
|
|
785
768
|
aria-label={`${title} ${value}%`}
|
|
786
|
-
|
|
769
|
+
initial={false}
|
|
770
|
+
animate={{ opacity: hiding ? 0 : 1 }}
|
|
771
|
+
transition={{ duration: 0.45, delay: hiding ? 0.35 : 0 }}
|
|
772
|
+
onAnimationComplete={() => hiding && setGone(true)}
|
|
773
|
+
className={`flex flex-col items-center justify-center gap-6 p-6 sm:p-8 ${fullScreen ? "fixed inset-0 z-[100]" : "relative h-full w-full"} ${hiding ? "pointer-events-none" : ""} ${dark ? "bg-neutral-950 text-white" : "bg-white text-neutral-900"} ${className}`}
|
|
787
774
|
style={dark ? { backgroundImage: `radial-gradient(60% 50% at 50% 40%, ${accentColor}14, transparent 70%)` } : undefined}
|
|
788
775
|
>
|
|
789
776
|
{game && (
|
|
@@ -798,8 +785,13 @@ const LoadingScreen = ({
|
|
|
798
785
|
{variant === "catch" && <CatchGame {...shared} />}
|
|
799
786
|
{variant === "runner" && <RunnerGame {...shared} />}
|
|
800
787
|
{variant === "racer" && <RacerGame {...shared} />}
|
|
801
|
-
{!game && variant !== "progress" &&
|
|
802
|
-
|
|
788
|
+
{!game && variant !== "progress" && (
|
|
789
|
+
<p className="text-center">
|
|
790
|
+
<span className="block text-[0.7rem] font-semibold uppercase tracking-[0.3em] opacity-50">{title}</span>
|
|
791
|
+
<span className="mt-1.5 block text-[0.75rem] opacity-60">{status ?? `${messageFor(value)}…`}</span>
|
|
792
|
+
</p>
|
|
793
|
+
)}
|
|
794
|
+
</motion.div>
|
|
803
795
|
);
|
|
804
796
|
};
|
|
805
797
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Real page-load progress, used by <LoadingScreen /> when you don't pass
|
|
5
|
+
* your own `progress`. It counts what the page is actually loading:
|
|
6
|
+
*
|
|
7
|
+
* - every <img> still loading (including ones added later),
|
|
8
|
+
* - scripts, stylesheets, fonts and fetches as the browser finishes them,
|
|
9
|
+
* - web fonts (document.fonts.ready) and the window "load" event.
|
|
10
|
+
*
|
|
11
|
+
* `progress` is finished / total (0 to 100) and only reaches 100 once the
|
|
12
|
+
* page has fully loaded and nothing new has started for a moment, so the
|
|
13
|
+
* bar and the count always match what really happened.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SETTLE_MS = 350;
|
|
17
|
+
|
|
18
|
+
export function usePageProgress(enabled = true) {
|
|
19
|
+
const [state, setState] = useState({ loaded: 0, total: 0, done: false });
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!enabled || typeof window === "undefined") return undefined;
|
|
23
|
+
let loaded = 0;
|
|
24
|
+
let total = 0;
|
|
25
|
+
let waitingForPage = document.readyState !== "complete";
|
|
26
|
+
let settle = 0;
|
|
27
|
+
let finished = false;
|
|
28
|
+
const seen = new WeakSet();
|
|
29
|
+
const cleanups = [];
|
|
30
|
+
|
|
31
|
+
const update = () => {
|
|
32
|
+
if (finished) return;
|
|
33
|
+
const idle = !waitingForPage && loaded >= total;
|
|
34
|
+
setState({ loaded, total, done: false });
|
|
35
|
+
clearTimeout(settle);
|
|
36
|
+
if (idle) {
|
|
37
|
+
// Wait a moment: a page often starts a few more requests right after load.
|
|
38
|
+
settle = setTimeout(() => {
|
|
39
|
+
if (!waitingForPage && loaded >= total) {
|
|
40
|
+
finished = true;
|
|
41
|
+
setState({ loaded, total: Math.max(total, loaded), done: true });
|
|
42
|
+
}
|
|
43
|
+
}, SETTLE_MS);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const add = (n = 1) => {
|
|
47
|
+
total += n;
|
|
48
|
+
update();
|
|
49
|
+
};
|
|
50
|
+
const finish = () => {
|
|
51
|
+
loaded += 1;
|
|
52
|
+
update();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Only what is still loading (or starts) after the loader appears is counted,
|
|
56
|
+
// so the bar runs from 0 to 100 for this load.
|
|
57
|
+
// Images: pending ones count until they load or fail.
|
|
58
|
+
const trackImage = (img) => {
|
|
59
|
+
if (seen.has(img)) return;
|
|
60
|
+
seen.add(img);
|
|
61
|
+
if (img.complete) return;
|
|
62
|
+
add();
|
|
63
|
+
const end = () => {
|
|
64
|
+
img.removeEventListener("load", end);
|
|
65
|
+
img.removeEventListener("error", end);
|
|
66
|
+
finish();
|
|
67
|
+
};
|
|
68
|
+
img.addEventListener("load", end);
|
|
69
|
+
img.addEventListener("error", end);
|
|
70
|
+
cleanups.push(() => {
|
|
71
|
+
img.removeEventListener("load", end);
|
|
72
|
+
img.removeEventListener("error", end);
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
Array.from(document.images).forEach(trackImage);
|
|
76
|
+
const mo = new MutationObserver((list) => {
|
|
77
|
+
for (const m of list) {
|
|
78
|
+
m.addedNodes.forEach((n) => {
|
|
79
|
+
if (n.nodeType !== 1) return;
|
|
80
|
+
if (n.tagName === "IMG") trackImage(n);
|
|
81
|
+
else n.querySelectorAll?.("img").forEach(trackImage);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
86
|
+
|
|
87
|
+
// Other files (scripts, CSS, fonts, fetch) are counted as the browser finishes them.
|
|
88
|
+
let po;
|
|
89
|
+
if (typeof PerformanceObserver !== "undefined") {
|
|
90
|
+
po = new PerformanceObserver((list) => {
|
|
91
|
+
const fresh = list.getEntries().filter((e) => e.initiatorType !== "img");
|
|
92
|
+
if (!fresh.length) return;
|
|
93
|
+
total += fresh.length;
|
|
94
|
+
loaded += fresh.length;
|
|
95
|
+
update();
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
po.observe({ type: "resource", buffered: false });
|
|
99
|
+
} catch {
|
|
100
|
+
po = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The page itself.
|
|
105
|
+
if (waitingForPage) {
|
|
106
|
+
total += 1;
|
|
107
|
+
const onLoad = () => {
|
|
108
|
+
waitingForPage = false;
|
|
109
|
+
finish();
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener("load", onLoad, { once: true });
|
|
112
|
+
cleanups.push(() => window.removeEventListener("load", onLoad));
|
|
113
|
+
}
|
|
114
|
+
// Web fonts.
|
|
115
|
+
if (document.fonts && document.fonts.status !== "loaded") {
|
|
116
|
+
total += 1;
|
|
117
|
+
document.fonts.ready.then(finish, finish);
|
|
118
|
+
}
|
|
119
|
+
update();
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
finished = true;
|
|
123
|
+
clearTimeout(settle);
|
|
124
|
+
mo.disconnect();
|
|
125
|
+
po?.disconnect();
|
|
126
|
+
cleanups.forEach((fn) => fn());
|
|
127
|
+
};
|
|
128
|
+
}, [enabled]);
|
|
129
|
+
|
|
130
|
+
const total = Math.max(1, state.total);
|
|
131
|
+
const progress = state.done ? 100 : Math.min(99, Math.round((state.loaded / total) * 100));
|
|
132
|
+
return { progress, loaded: state.loaded, total: state.total, done: state.done };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default usePageProgress;
|
package/package.json
CHANGED