apex-ui-kit 2.6.0 → 2.8.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 +32 -10
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +340 -161
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +356 -0
- package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +22 -2
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +381 -0
- package/dist/ApexUI-Kit/templates/CommandPalette/CommandPalette.jsx +250 -0
- package/dist/ApexUI-Kit/templates/CrossedMarquee/CrossedMarquee.jsx +158 -0
- package/dist/ApexUI-Kit/templates/DitherWaves/DitherWaves.jsx +207 -0
- package/dist/ApexUI-Kit/templates/EdgeBlur/EdgeBlur.jsx +92 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +5 -1
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +274 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HorizonGlow/HorizonGlow.jsx +259 -0
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +116 -0
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +230 -0
- package/dist/ApexUI-Kit/templates/MagneticButton/MagneticButton.jsx +69 -0
- package/dist/ApexUI-Kit/templates/Marquee3D/Marquee3D.jsx +68 -0
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +123 -1
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
- package/dist/ApexUI-Kit/templates/ParticleText/ParticleText.jsx +328 -0
- package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +119 -0
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +88 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +22 -1
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +290 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +18 -3
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +215 -0
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/themes.js +38 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +21 -13
- package/dist/ApexUI-Kit/templates/WireframeKnot3D/WireframeKnot3D.jsx +112 -0
- package/dist/cli/index.js +20 -0
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.css +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components -- the hook and helpers ship with the component on purpose */
|
|
2
|
+
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
5
|
+
import { applyTheme, defaultThemes } from "./themes.js";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Theme Switcher: a palette button that opens a grid of colour themes. Picking
|
|
9
|
+
* one reveals it with a soft circle growing from the swatch you clicked (View
|
|
10
|
+
* Transitions API), and every component calling useTheme stays in sync.
|
|
11
|
+
*
|
|
12
|
+
* Props: themes = defaultThemes, placement = "down" | "up", align = "right" |
|
|
13
|
+
* "left", label (text on the trigger; without it the trigger is a round icon
|
|
14
|
+
* button), storageKey = "theme", target (element or ref to theme instead of
|
|
15
|
+
* <html>, e.g. one preview box), className, buttonClassName.
|
|
16
|
+
*
|
|
17
|
+
* Hook: const [themeId, setTheme] = useTheme(themes, { storageKey, defaultTheme });
|
|
18
|
+
* setTheme("grape", event.currentTarget) reveals from that element.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export { defaultThemes, applyTheme, themeScript } from "./themes.js";
|
|
22
|
+
|
|
23
|
+
const REVEAL_CSS = `@property --reveal { syntax: '<length>'; inherits: false; initial-value: 0px; }
|
|
24
|
+
@keyframes theme-reveal { from { --reveal: -10vmax; } to { --reveal: 160vmax; } }
|
|
25
|
+
html[data-theme-switching]::view-transition-old(root) { animation: none; }
|
|
26
|
+
html[data-theme-switching]::view-transition-new(root) {
|
|
27
|
+
mix-blend-mode: normal;
|
|
28
|
+
mask-image: radial-gradient(circle at var(--reveal-x, 100%) var(--reveal-y, 0px), #000 var(--reveal), transparent calc(var(--reveal) + 10vmax));
|
|
29
|
+
animation: theme-reveal 0.9s cubic-bezier(0.65, 0, 0.35, 1) both;
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
const injectCss = () => {
|
|
33
|
+
if (typeof document === "undefined" || document.getElementById("apex-theme-reveal")) return;
|
|
34
|
+
const style = document.createElement("style");
|
|
35
|
+
style.id = "apex-theme-reveal";
|
|
36
|
+
style.textContent = REVEAL_CSS;
|
|
37
|
+
document.head.appendChild(style);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const read = (key) => {
|
|
41
|
+
try {
|
|
42
|
+
return localStorage.getItem(key);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const write = (key, value) => {
|
|
48
|
+
try {
|
|
49
|
+
localStorage.setItem(key, value);
|
|
50
|
+
} catch {
|
|
51
|
+
/* storage blocked: the theme still applies for this visit */
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// One small store per storage key, so every useTheme with that key shares state.
|
|
56
|
+
const stores = new Map();
|
|
57
|
+
function storeFor(key, themes, defaultTheme) {
|
|
58
|
+
if (stores.has(key)) return stores.get(key);
|
|
59
|
+
const prefersDark = typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
60
|
+
const saved = read(key);
|
|
61
|
+
const initial = (themes.find((x) => x.id === saved) ?? themes.find((x) => x.id === defaultTheme) ?? themes.find((x) => x.scheme === (prefersDark ? "dark" : "light")) ?? themes[0])?.id;
|
|
62
|
+
const subs = new Set();
|
|
63
|
+
const store = {
|
|
64
|
+
id: initial,
|
|
65
|
+
get: () => store.id,
|
|
66
|
+
subscribe: (fn) => {
|
|
67
|
+
subs.add(fn);
|
|
68
|
+
return () => subs.delete(fn);
|
|
69
|
+
},
|
|
70
|
+
set: (id) => {
|
|
71
|
+
store.id = id;
|
|
72
|
+
subs.forEach((fn) => fn());
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
stores.set(key, store);
|
|
76
|
+
return store;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useTheme(themes = defaultThemes, { storageKey = "theme", defaultTheme, target } = {}) {
|
|
80
|
+
const store = storeFor(storageKey, themes, defaultTheme);
|
|
81
|
+
const id = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
82
|
+
const rootEl = () => target?.current ?? (target instanceof Element ? target : null) ?? document.documentElement;
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
injectCss();
|
|
86
|
+
applyTheme(themes.find((x) => x.id === store.get()), rootEl());
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [themes, store, id]);
|
|
89
|
+
|
|
90
|
+
const setTheme = (next, origin) => {
|
|
91
|
+
const theme = themes.find((x) => x.id === next);
|
|
92
|
+
if (!theme || next === store.get()) return;
|
|
93
|
+
write(storageKey, next);
|
|
94
|
+
const commit = () => {
|
|
95
|
+
applyTheme(theme, rootEl());
|
|
96
|
+
flushSync(() => store.set(next));
|
|
97
|
+
};
|
|
98
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
99
|
+
if (!document.startViewTransition || reduced) {
|
|
100
|
+
commit();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const root = document.documentElement;
|
|
104
|
+
if (origin?.getBoundingClientRect) {
|
|
105
|
+
const r = origin.getBoundingClientRect();
|
|
106
|
+
root.style.setProperty("--reveal-x", `${r.left + r.width / 2}px`);
|
|
107
|
+
root.style.setProperty("--reveal-y", `${r.top + r.height / 2}px`);
|
|
108
|
+
} else {
|
|
109
|
+
root.style.setProperty("--reveal-x", "100%");
|
|
110
|
+
root.style.setProperty("--reveal-y", "0px");
|
|
111
|
+
}
|
|
112
|
+
root.setAttribute("data-theme-switching", "");
|
|
113
|
+
const transition = document.startViewTransition(commit);
|
|
114
|
+
transition.ready.catch(() => {});
|
|
115
|
+
transition.finished.catch(() => {}).finally(() => root.removeAttribute("data-theme-switching"));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return [id, setTheme];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const Palette = () => (
|
|
122
|
+
<svg viewBox="0 0 24 24" className="h-[18px] w-[18px]" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
123
|
+
<path d="M12 22a10 10 0 1 1 10-10c0 2.8-2.2 4-4 4h-1.8a2 2 0 0 0-1.5 3.3A1.6 1.6 0 0 1 12 22z" />
|
|
124
|
+
<circle cx="7.5" cy="10.5" r="1" fill="currentColor" />
|
|
125
|
+
<circle cx="10.5" cy="6.5" r="1" fill="currentColor" />
|
|
126
|
+
<circle cx="15.5" cy="7.5" r="1" fill="currentColor" />
|
|
127
|
+
</svg>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const ThemeSwitcher = ({ themes = defaultThemes, placement = "down", align = "right", label, storageKey = "theme", target, className = "", buttonClassName = "" }) => {
|
|
131
|
+
const [id, setTheme] = useTheme(themes, { storageKey, target });
|
|
132
|
+
const [open, setOpen] = useState(false);
|
|
133
|
+
const root = useRef(null);
|
|
134
|
+
const current = themes.find((x) => x.id === id) ?? themes[0];
|
|
135
|
+
const up = placement === "up";
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!open) return undefined;
|
|
139
|
+
const down = (e) => !root.current?.contains(e.target) && setOpen(false);
|
|
140
|
+
const key = (e) => e.key === "Escape" && setOpen(false);
|
|
141
|
+
document.addEventListener("pointerdown", down);
|
|
142
|
+
document.addEventListener("keydown", key);
|
|
143
|
+
return () => {
|
|
144
|
+
document.removeEventListener("pointerdown", down);
|
|
145
|
+
document.removeEventListener("keydown", key);
|
|
146
|
+
};
|
|
147
|
+
}, [open]);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div ref={root} className={`relative inline-block ${className}`}>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label="Choose colour theme"
|
|
154
|
+
aria-haspopup="true"
|
|
155
|
+
aria-expanded={open}
|
|
156
|
+
onClick={() => setOpen((o) => !o)}
|
|
157
|
+
className={`relative inline-flex items-center justify-center gap-2 border border-[var(--line)] bg-[var(--surface)] text-[var(--ink)] transition-colors hover:border-[var(--accent)] ${label ? "h-9 rounded-full px-3.5 text-sm font-medium" : "h-9 w-9 rounded-full"} ${buttonClassName}`}
|
|
158
|
+
>
|
|
159
|
+
<Palette />
|
|
160
|
+
{label && <span>{label}</span>}
|
|
161
|
+
<span aria-hidden="true" className="absolute right-0.5 top-0.5 h-2 w-2 rounded-full ring-2 ring-[var(--surface)]" style={{ background: current?.vars["--accent"] }} />
|
|
162
|
+
</button>
|
|
163
|
+
<AnimatePresence>
|
|
164
|
+
{open && (
|
|
165
|
+
<motion.div
|
|
166
|
+
role="dialog"
|
|
167
|
+
aria-label="Colour themes"
|
|
168
|
+
initial={{ opacity: 0, y: up ? 8 : -8, scale: 0.96 }}
|
|
169
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
170
|
+
exit={{ opacity: 0, y: up ? 8 : -8, scale: 0.96 }}
|
|
171
|
+
transition={{ type: "spring", stiffness: 380, damping: 28 }}
|
|
172
|
+
className={`absolute z-50 w-[17.5rem] max-w-[calc(100vw-2rem)] rounded-2xl border border-[var(--line)] bg-[var(--surface)] p-3 text-[var(--ink)] shadow-[0_24px_60px_-16px_rgba(0,0,0,0.35)] ${up ? "bottom-full mb-2" : "top-full mt-2"} ${align === "left" ? "left-0" : "right-0"}`}
|
|
173
|
+
style={{ transformOrigin: `${up ? "bottom" : "top"} ${align === "left" ? "left" : "right"}` }}
|
|
174
|
+
>
|
|
175
|
+
<p className="mb-2.5 px-1 font-mono text-[0.65rem] uppercase tracking-[0.16em] text-[var(--muted)]">Pick a colour vibe</p>
|
|
176
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
177
|
+
{themes.map((theme, i) => {
|
|
178
|
+
const active = theme.id === id;
|
|
179
|
+
return (
|
|
180
|
+
<motion.button
|
|
181
|
+
key={theme.id}
|
|
182
|
+
type="button"
|
|
183
|
+
aria-pressed={active}
|
|
184
|
+
initial={{ y: 6 }}
|
|
185
|
+
animate={{ y: 0 }}
|
|
186
|
+
transition={{ delay: 0.03 * i, type: "spring", stiffness: 380, damping: 28 }}
|
|
187
|
+
onClick={(e) => {
|
|
188
|
+
setTheme(theme.id, e.currentTarget.querySelector("[data-swatch]"));
|
|
189
|
+
setOpen(false);
|
|
190
|
+
}}
|
|
191
|
+
className={`relative flex items-center gap-2.5 rounded-xl py-2 pl-2 pr-5 text-left text-[0.82rem] font-medium transition-colors hover:bg-[var(--bg)] ${active ? "bg-[var(--bg)]" : ""}`}
|
|
192
|
+
style={{ animation: `apex-theme-in 240ms ease-out ${30 * i}ms backwards` }}
|
|
193
|
+
>
|
|
194
|
+
<span data-swatch="" className="grid h-7 w-7 shrink-0 place-items-center rounded-full border border-[var(--line)]" style={{ background: theme.vars["--bg"] }}>
|
|
195
|
+
<span className="h-3 w-3 rounded-full" style={{ background: theme.vars["--accent"] }} />
|
|
196
|
+
</span>
|
|
197
|
+
<span className="flex-1 truncate">{theme.name}</span>
|
|
198
|
+
{active && (
|
|
199
|
+
<svg viewBox="0 0 24 24" className="absolute right-1.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--accent)]" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
200
|
+
<path d="M5 12l5 5L20 7" />
|
|
201
|
+
</svg>
|
|
202
|
+
)}
|
|
203
|
+
</motion.button>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
</div>
|
|
207
|
+
<style>{"@keyframes apex-theme-in{from{opacity:0}to{opacity:1}}"}</style>
|
|
208
|
+
</motion.div>
|
|
209
|
+
)}
|
|
210
|
+
</AnimatePresence>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export default ThemeSwitcher;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Default themes for ThemeSwitcher, plus helpers to apply one and to inline
|
|
3
|
+
* a no-flash script in index.html. Each theme sets seven CSS variables on
|
|
4
|
+
* <html>: --bg, --surface, --ink, --muted, --line, --accent, --accent-ink.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const t = (id, name, scheme, bg, surface, ink, muted, line, accent, accentInk) => ({
|
|
8
|
+
id,
|
|
9
|
+
name,
|
|
10
|
+
scheme,
|
|
11
|
+
vars: { "--bg": bg, "--surface": surface, "--ink": ink, "--muted": muted, "--line": line, "--accent": accent, "--accent-ink": accentInk },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const defaultThemes = [
|
|
15
|
+
t("sunrise", "Sunrise", "light", "#f4f2ed", "#ffffff", "#121110", "#6f6b64", "rgb(18 17 16 / .12)", "#ff5b1f", "#ffffff"),
|
|
16
|
+
t("ember", "Ember", "dark", "#0e0d0c", "#171615", "#f1efe9", "#9a958c", "rgb(241 239 233 / .12)", "#ff7a3d", "#120a05"),
|
|
17
|
+
t("paper", "Paper", "light", "#fafaf7", "#ffffff", "#111111", "#6b6b66", "rgb(17 17 17 / .12)", "#111111", "#fafaf7"),
|
|
18
|
+
t("noir", "Noir", "dark", "#0c0c0c", "#161616", "#f5f5f2", "#8f8f8a", "rgb(245 245 242 / .12)", "#f5f5f2", "#0c0c0c"),
|
|
19
|
+
t("meadow", "Meadow", "light", "#f2f7ef", "#ffffff", "#10200f", "#5d6f5a", "rgb(16 32 15 / .12)", "#2fb562", "#ffffff"),
|
|
20
|
+
t("matrix", "Matrix", "dark", "#07110a", "#0d1a11", "#d7f5dd", "#6f9a78", "rgb(215 245 221 / .12)", "#4ade80", "#04130a"),
|
|
21
|
+
t("ocean", "Ocean", "light", "#eef3fa", "#ffffff", "#0c1a2b", "#5b6b80", "rgb(12 26 43 / .12)", "#2563eb", "#ffffff"),
|
|
22
|
+
t("grape", "Grape", "dark", "#0f0b1a", "#181229", "#ece8f7", "#9890b0", "rgb(236 232 247 / .12)", "#a78bfa", "#140d24"),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function applyTheme(theme, root = typeof document !== "undefined" ? document.documentElement : null) {
|
|
26
|
+
if (!theme || !root) return;
|
|
27
|
+
Object.entries(theme.vars).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
28
|
+
root.setAttribute("data-theme", theme.id);
|
|
29
|
+
root.setAttribute("data-scheme", theme.scheme);
|
|
30
|
+
root.style.colorScheme = theme.scheme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A string to inline in <head> (before your app script) so the saved theme
|
|
34
|
+
// is applied before React loads: no flash of the wrong colours.
|
|
35
|
+
export function themeScript(storageKey = "theme", themes = defaultThemes) {
|
|
36
|
+
const data = JSON.stringify(themes.map(({ id, scheme, vars }) => ({ id, scheme, vars })));
|
|
37
|
+
return `(function(){try{var T=${data};var id=localStorage.getItem(${JSON.stringify(storageKey)});var t=T.find(function(x){return x.id===id});if(!t)return;var r=document.documentElement;for(var k in t.vars)r.style.setProperty(k,t.vars[k]);r.setAttribute("data-theme",t.id);r.setAttribute("data-scheme",t.scheme);r.style.colorScheme=t.scheme;}catch(e){}})();`;
|
|
38
|
+
}
|
|
@@ -66,6 +66,7 @@ const ToastStack = forwardRef(function ToastStack({ position = "bottom-right", d
|
|
|
66
66
|
onPointerLeave={() => setHovered(false)}
|
|
67
67
|
className={`${contained ? "absolute" : "fixed"} z-[200] flex w-[min(22rem,calc(100%-2rem))] flex-col ${place} ${className}`}
|
|
68
68
|
>
|
|
69
|
+
<style>{"@keyframes apex-toast-in{from{opacity:0}to{opacity:1}}"}</style>
|
|
69
70
|
<AnimatePresence initial={false}>
|
|
70
71
|
{toasts.map((t, i) => {
|
|
71
72
|
const fromEnd = toasts.length - 1 - i;
|
|
@@ -75,27 +76,34 @@ const ToastStack = forwardRef(function ToastStack({ position = "bottom-right", d
|
|
|
75
76
|
return (
|
|
76
77
|
<motion.li
|
|
77
78
|
key={t.id}
|
|
78
|
-
layout
|
|
79
79
|
drag="x"
|
|
80
80
|
dragConstraints={{ left: 0, right: 0 }}
|
|
81
81
|
dragElastic={0.6}
|
|
82
82
|
onDragEnd={(_, info) => Math.abs(info.offset.x) > 90 && dismiss(t.id)}
|
|
83
|
-
initial={{
|
|
84
|
-
animate={{
|
|
85
|
-
exit={{
|
|
83
|
+
initial={{ y: top ? -30 : 30, scale: 0.94 }}
|
|
84
|
+
animate={{ y: top ? offset : -offset, scale: hovered ? 1 : 1 - fromEnd * 0.05 }}
|
|
85
|
+
exit={{ scale: 0.9, transition: { duration: 0.18 } }}
|
|
86
86
|
transition={{ type: "spring", stiffness: 380, damping: 32 }}
|
|
87
87
|
className={`absolute w-full cursor-grab touch-pan-y list-none active:cursor-grabbing ${top ? "top-0" : "bottom-0"}`}
|
|
88
88
|
style={{ zIndex: i }}
|
|
89
89
|
>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
{/* Cards deeper than three fade out of the collapsed stack. */}
|
|
91
|
+
<div className="transition-opacity duration-300" style={{ opacity: fromEnd > 2 && !hovered ? 0 : 1 }}>
|
|
92
|
+
{/* The fade-in is a CSS animation: framer hands opacity to the browser
|
|
93
|
+
and, when that finishes, can show the start value for one frame. */}
|
|
94
|
+
<motion.div
|
|
95
|
+
exit={{ opacity: 0, transition: { duration: 0.18 } }}
|
|
96
|
+
style={{ animation: "apex-toast-in 0.22s ease-out backwards" }}
|
|
97
|
+
className={`relative flex items-start gap-3 rounded-2xl border p-3.5 pr-9 shadow-[0_18px_40px_-18px_rgba(0,0,0,0.5)] backdrop-blur ${dark ? "border-white/10 bg-neutral-900/95 text-white" : "border-black/10 bg-white/95 text-neutral-900"}`}>
|
|
98
|
+
<Icon className="mt-0.5 h-5 w-5 shrink-0" style={{ color }} />
|
|
99
|
+
<div className="min-w-0">
|
|
100
|
+
<p className="text-[0.88rem] font-semibold">{t.title}</p>
|
|
101
|
+
{t.description && <p className={`mt-0.5 text-[0.8rem] ${dark ? "text-white/60" : "text-neutral-500"}`}>{t.description}</p>}
|
|
102
|
+
</div>
|
|
103
|
+
<button type="button" aria-label="Dismiss" onClick={() => dismiss(t.id)} className={`absolute right-2.5 top-2.5 grid h-6 w-6 place-items-center rounded-full ${dark ? "text-white/50 hover:bg-white/10 hover:text-white" : "text-neutral-400 hover:bg-black/5 hover:text-neutral-900"}`}>
|
|
104
|
+
<X className="h-3.5 w-3.5" />
|
|
105
|
+
</button>
|
|
106
|
+
</motion.div>
|
|
99
107
|
</div>
|
|
100
108
|
</motion.li>
|
|
101
109
|
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { AdditiveBlending, Color, IcosahedronGeometry, LineBasicMaterial, LineSegments, PerspectiveCamera, Points, PointsMaterial, Scene, TorusKnotGeometry, WebGLRenderer, WireframeGeometry } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Wireframe Knot 3D: a glowing wireframe shape (torus knot, sphere or
|
|
6
|
+
* icosahedron) with bright points at its corners, slowly turning and
|
|
7
|
+
* leaning toward the pointer. Additive lines make it glow without any
|
|
8
|
+
* post-processing, so it stays light. Pauses off screen and frees the GPU
|
|
9
|
+
* on unmount.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SHAPES = {
|
|
13
|
+
knot: () => new TorusKnotGeometry(1, 0.32, 140, 14, 2, 3),
|
|
14
|
+
sphere: () => new IcosahedronGeometry(1.35, 3),
|
|
15
|
+
gem: () => new IcosahedronGeometry(1.4, 1),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const WireframeKnot3D = ({ shape = "knot", color = "#a3e635", pointColor = "#ffffff", speed = 1, interactive = true, size = 24, className = "" }) => {
|
|
19
|
+
const mount = useRef(null);
|
|
20
|
+
const opts = useRef({ color, pointColor, speed, interactive });
|
|
21
|
+
opts.current = { color, pointColor, speed, interactive };
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const el = mount.current;
|
|
25
|
+
const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
26
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
27
|
+
Object.assign(renderer.domElement.style, { display: "block", width: "100%", height: "100%" });
|
|
28
|
+
el.appendChild(renderer.domElement);
|
|
29
|
+
const scene = new Scene();
|
|
30
|
+
const camera = new PerspectiveCamera(40, 1, 0.1, 50);
|
|
31
|
+
camera.position.z = 5.2;
|
|
32
|
+
|
|
33
|
+
const geometry = (SHAPES[shape] ?? SHAPES.knot)();
|
|
34
|
+
const wire = new WireframeGeometry(geometry);
|
|
35
|
+
const lineMat = new LineBasicMaterial({ color: new Color(color), transparent: true, opacity: 0.55, blending: AdditiveBlending, depthWrite: false });
|
|
36
|
+
const lines = new LineSegments(wire, lineMat);
|
|
37
|
+
const pointMat = new PointsMaterial({ color: new Color(pointColor), size: 0.035, transparent: true, opacity: 0.9, blending: AdditiveBlending, depthWrite: false });
|
|
38
|
+
const points = new Points(geometry, pointMat);
|
|
39
|
+
const group = new Scene();
|
|
40
|
+
group.add(lines, points);
|
|
41
|
+
scene.add(group);
|
|
42
|
+
|
|
43
|
+
const resize = () => {
|
|
44
|
+
const { width, height } = el.getBoundingClientRect();
|
|
45
|
+
if (!width || !height) return;
|
|
46
|
+
renderer.setSize(width, height, false);
|
|
47
|
+
camera.aspect = width / height;
|
|
48
|
+
camera.updateProjectionMatrix();
|
|
49
|
+
};
|
|
50
|
+
const ro = new ResizeObserver(resize);
|
|
51
|
+
ro.observe(el);
|
|
52
|
+
resize();
|
|
53
|
+
|
|
54
|
+
const pointer = { x: 0, y: 0 };
|
|
55
|
+
const onMove = (e) => {
|
|
56
|
+
const r = el.getBoundingClientRect();
|
|
57
|
+
pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
58
|
+
pointer.y = ((e.clientY - r.top) / r.height) * 2 - 1;
|
|
59
|
+
};
|
|
60
|
+
const onLeave = () => {
|
|
61
|
+
pointer.x = 0;
|
|
62
|
+
pointer.y = 0;
|
|
63
|
+
};
|
|
64
|
+
el.addEventListener("pointermove", onMove);
|
|
65
|
+
el.addEventListener("pointerleave", onLeave);
|
|
66
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
67
|
+
let visible = true;
|
|
68
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
69
|
+
io.observe(el);
|
|
70
|
+
|
|
71
|
+
let raf = 0;
|
|
72
|
+
let last = performance.now();
|
|
73
|
+
let spin = 0;
|
|
74
|
+
const frame = (now) => {
|
|
75
|
+
raf = requestAnimationFrame(frame);
|
|
76
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
77
|
+
last = now;
|
|
78
|
+
if (!visible) return;
|
|
79
|
+
const o = opts.current;
|
|
80
|
+
spin += dt * 0.35 * o.speed * (reduced ? 0.15 : 1);
|
|
81
|
+
const px = o.interactive ? pointer.y * 0.6 : 0;
|
|
82
|
+
const py = o.interactive ? pointer.x * 0.8 : 0;
|
|
83
|
+
group.rotation.x += (px + spin * 0.5 - group.rotation.x) * 0.06;
|
|
84
|
+
group.rotation.y += (py + spin - group.rotation.y) * 0.06;
|
|
85
|
+
lineMat.color.set(o.color);
|
|
86
|
+
pointMat.color.set(o.pointColor);
|
|
87
|
+
renderer.render(scene, camera);
|
|
88
|
+
};
|
|
89
|
+
raf = requestAnimationFrame(frame);
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
cancelAnimationFrame(raf);
|
|
93
|
+
ro.disconnect();
|
|
94
|
+
io.disconnect();
|
|
95
|
+
el.removeEventListener("pointermove", onMove);
|
|
96
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
97
|
+
geometry.dispose();
|
|
98
|
+
wire.dispose();
|
|
99
|
+
lineMat.dispose();
|
|
100
|
+
pointMat.dispose();
|
|
101
|
+
renderer.dispose();
|
|
102
|
+
renderer.forceContextLoss();
|
|
103
|
+
renderer.domElement.remove();
|
|
104
|
+
};
|
|
105
|
+
// Colors, speed and interaction are read live; only the shape rebuilds.
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
}, [shape]);
|
|
108
|
+
|
|
109
|
+
return <div ref={mount} aria-hidden="true" className={`relative touch-none ${className}`} style={{ width: `min(${size}rem, 90vw)`, aspectRatio: "1" }} />;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default WireframeKnot3D;
|
package/dist/cli/index.js
CHANGED
|
@@ -120,6 +120,25 @@ const componentMap = {
|
|
|
120
120
|
"distortion-image": "DistortionImage",
|
|
121
121
|
"galaxy-3d": "Galaxy3D",
|
|
122
122
|
"warp-tunnel": "WarpTunnel",
|
|
123
|
+
"magnetic-button": "MagneticButton",
|
|
124
|
+
"radial-menu": "RadialMenu",
|
|
125
|
+
"marquee-3d": "Marquee3D",
|
|
126
|
+
"wireframe-knot-3d": "WireframeKnot3D",
|
|
127
|
+
"particle-text": "ParticleText",
|
|
128
|
+
"edge-blur": "EdgeBlur",
|
|
129
|
+
"circular-carousel": "CircularCarousel",
|
|
130
|
+
"arc-gallery": "ArcGallery",
|
|
131
|
+
"scroll-stack": "ScrollStack",
|
|
132
|
+
"sunrise-background": "SunriseBackground",
|
|
133
|
+
"horizon-glow": "HorizonGlow",
|
|
134
|
+
"glass-lens": "GlassLens",
|
|
135
|
+
"dither-waves": "DitherWaves",
|
|
136
|
+
"infinite-canvas": "InfiniteCanvas",
|
|
137
|
+
"image-accordion": "ImageAccordion",
|
|
138
|
+
"orbit-icons": "OrbitIcons",
|
|
139
|
+
"command-palette": "CommandPalette",
|
|
140
|
+
"crossed-marquee": "CrossedMarquee",
|
|
141
|
+
"theme-switcher": "ThemeSwitcher",
|
|
123
142
|
"water-drop-reveal": "MaskView",
|
|
124
143
|
};
|
|
125
144
|
// Packages only some components need, installed on top of the shared ones.
|
|
@@ -129,6 +148,7 @@ const extraDeps = {
|
|
|
129
148
|
SmokeBackground: ["ogl"],
|
|
130
149
|
LiquidGradient: ["ogl"],
|
|
131
150
|
WebGLCarousel: ["ogl"],
|
|
151
|
+
CrossedMarquee: ["simple-icons"],
|
|
132
152
|
};
|
|
133
153
|
async function main() {
|
|
134
154
|
const args = process.argv.slice(2);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apex-ui-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "115 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
|
|
6
6
|
"homepage": "https://apex-ui.in",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@import url("https://fonts.googleapis.com/css2?family=Righteous&display=swap");
|