apex-ui-kit 2.7.0 → 2.9.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.
Files changed (24) hide show
  1. package/ReadMe.md +21 -9
  2. package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
  3. package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +29 -11
  4. package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +0 -1
  5. package/dist/ApexUI-Kit/templates/CommandPalette/CommandPalette.jsx +250 -0
  6. package/dist/ApexUI-Kit/templates/CrossedMarquee/CrossedMarquee.jsx +158 -0
  7. package/dist/ApexUI-Kit/templates/DitherWaves/DitherWaves.jsx +207 -0
  8. package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +3 -2
  9. package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +25 -18
  10. package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +273 -0
  11. package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +1 -1
  12. package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +116 -0
  13. package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +230 -0
  14. package/dist/ApexUI-Kit/templates/LiquidGlass/LiquidGlass.jsx +444 -0
  15. package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
  16. package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +20 -4
  17. package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +3 -3
  18. package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +68 -2
  19. package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +18 -3
  20. package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +215 -0
  21. package/dist/ApexUI-Kit/templates/ThemeSwitcher/themes.js +38 -0
  22. package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +21 -13
  23. package/dist/cli/index.js +10 -0
  24. package/package.json +2 -2
@@ -5,8 +5,8 @@ import React, { Children, useLayoutEffect, useRef } from "react";
5
5
  * card that gets covered shrinks a little, darkens and (optionally) blurs or
6
6
  * leans back, so the pile reads as depth. It rides on native scrolling
7
7
  * (position: sticky + one rAF update per scroll), so it stays smooth with
8
- * mouse, trackpad, touch and keyboard. Scrolls inside its own box, or with
9
- * the page (useWindowScroll).
8
+ * mouse, trackpad, touch and keyboard. By default it stacks with the page
9
+ * scroll; set useWindowScroll={false} to stack inside its own scroll box.
10
10
  */
11
11
 
12
12
  export const ScrollStackItem = ({ children, className = "", style }) => (
@@ -16,7 +16,7 @@ export const ScrollStackItem = ({ children, className = "", style }) => (
16
16
  </div>
17
17
  );
18
18
 
19
- const ScrollStack = ({ children, gap = 90, offset = 22, top = 64, scaleStep = 0.05, minScale = 0.82, dim = 0.22, blur = 0, tilt = 0, endSpace = 180, useWindowScroll = false, height = "100%", className = "" }) => {
19
+ const ScrollStack = ({ children, gap = 90, offset = 22, top = 64, scaleStep = 0.05, minScale = 0.82, dim = 0.22, blur = 0, tilt = 0, endSpace = 180, useWindowScroll = true, height = "100%", className = "" }) => {
20
20
  const root = useRef(null);
21
21
  const slots = useRef([]);
22
22
  const items = Children.toArray(children);
@@ -23,6 +23,7 @@ precision highp float;
23
23
  uniform vec2 uRes; uniform float uTime; uniform float uRise; uniform vec2 uMouse;
24
24
  uniform vec3 uSun; uniform vec3 uSkyTop; uniform vec3 uSkyLow; uniform vec3 uMount;
25
25
  uniform float uRays; uniform float uStars; uniform float uHaze; uniform float uSunSize; uniform float uSunHeight;
26
+ uniform float uTrees; uniform float uBirds;
26
27
 
27
28
  float h1(float x){ return fract(sin(x * 127.1) * 43758.5453); }
28
29
  float h2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
@@ -47,6 +48,23 @@ float range(float x, float c, float w, float peak, float base, float amp, float
47
48
  return base + s * peak + (crag(x * freq + seed) - 0.45) * amp * (0.35 + s);
48
49
  }
49
50
 
51
+ // A pine silhouette: three stacked tiers, each wider at its base, on a short trunk.
52
+ // dx / dy are measured from the foot of the tree, h is its height (all in screen units).
53
+ float pine(float dx, float dy, float h){
54
+ if (h <= 0.0) return 0.0;
55
+ float t = dy / h;
56
+ if (t < 0.0 || t > 1.0) return 0.0;
57
+ float aa = 0.0015;
58
+ float trunk = smoothstep(h * 0.035 + aa, h * 0.035 - aa, abs(dx)) * step(t, 0.14);
59
+ float tiers = 3.0;
60
+ float k = floor(min((t - 0.1) / 0.9, 0.999) * tiers);
61
+ float local = fract(min((t - 0.1) / 0.9, 0.999) * tiers);
62
+ float base = 0.36 * h * (1.0 - k / tiers * 0.62);
63
+ float w = base * (1.0 - local * 0.78);
64
+ float crown = smoothstep(w + aa, w - aa, abs(dx)) * step(0.1, t);
65
+ return max(trunk, crown);
66
+ }
67
+
50
68
  void main(){
51
69
  vec2 uv = gl_FragCoord.xy / uRes;
52
70
  float asp = uRes.x / uRes.y;
@@ -114,6 +132,46 @@ void main(){
114
132
  cn += uSun * exp(-max(hn - y, 0.0) * 120.0) * toSun * 0.9;
115
133
  col = mix(col, cn, mn);
116
134
 
135
+ // Pines along the near ridge, thinning out toward the valley so the sun stays clear.
136
+ // Sizes are in screen-height units so a tree keeps its shape on any screen.
137
+ float unit = 0.5 * clamp(asp, 0.7, 2.2); // one xn unit in height units
138
+ float tx = (xn + mx * 0.05) * 26.0;
139
+ float tcell = floor(tx);
140
+ float cellW = unit / 26.0;
141
+ float jitter = (h1(tcell + 1.9) - 0.5) * 0.3;
142
+ float dx = (fract(tx) - 0.5 - jitter) * cellW;
143
+ float pres = step(0.3, h1(tcell + 3.1)) * smoothstep(0.1, 0.42, abs(xn));
144
+ float ht = min(0.022 + 0.04 * h1(tcell + 7.7), cellW * 1.25) * pres * uTrees;
145
+ float trees = pine(dx, y - hn + 0.004, ht);
146
+ col = mix(col, uMount * 0.6 + uSun * toSun * 0.05, trees * (1.0 - mn));
147
+
148
+ // A few big pines framing the bottom corners.
149
+ float tx2 = (xn + mx * 0.08) * 6.0;
150
+ float cell2 = floor(tx2);
151
+ float cellW2 = unit / 6.0;
152
+ float dx2 = (fract(tx2) - 0.5) * cellW2;
153
+ float ht2 = min(0.16 + 0.1 * h1(cell2 + 1.3), cellW2 * 1.3) * step(0.35, h1(cell2 + 4.4)) * smoothstep(0.62, 0.78, abs(xn)) * uTrees;
154
+ col = mix(col, uMount * 0.38, pine(dx2, y + 0.015, ht2));
155
+
156
+ // A small flock crossing the sky, wings flapping out of step.
157
+ if (uBirds > 0.0) {
158
+ float lap = fract(uTime * 0.03 + 0.38);
159
+ vec2 flock = vec2(mix(-asp * 0.5 - 0.25, asp * 0.5 + 0.25, lap), 0.66 + 0.035 * sin(uTime * 0.31));
160
+ for (int i = 0; i < 7; i++) {
161
+ float fi = float(i);
162
+ if (fi >= uBirds * 7.0) break;
163
+ float side = mod(fi, 2.0) * 2.0 - 1.0;
164
+ float rank = floor((fi + 1.0) / 2.0);
165
+ vec2 bpos = flock + vec2(-rank * 0.045, -rank * 0.022 * side) + vec2(0.006 * sin(uTime * 1.3 + fi), 0.008 * sin(uTime * 0.9 + fi * 1.7));
166
+ float size = 0.016 * (1.0 - rank * 0.08);
167
+ vec2 r = q - bpos;
168
+ float flap = 0.15 + 0.75 * (0.5 + 0.5 * sin(uTime * 9.0 + fi * 1.9));
169
+ float dist = abs(r.y - abs(r.x) * flap + size * 0.15) - 0.0006;
170
+ float bird = smoothstep(0.0024, 0.0008, dist) * step(abs(r.x), size);
171
+ col = mix(col, uMount * 0.45, bird * smoothstep(0.2, 0.5, rise));
172
+ }
173
+ }
174
+
117
175
  // Low fog over everything and light bleeding over the ridges.
118
176
  col += haze * exp(-y * 7.0) * 0.12 * uHaze * rise;
119
177
  col += uSun * exp(-d * 3.0) * 0.18 * rise;
@@ -132,7 +190,9 @@ const SunriseBackground = ({
132
190
  mountainColor,
133
191
  duration = 6,
134
192
  autoRise = true,
135
- sunHeight = 0.36,
193
+ sunHeight = 0.28,
194
+ trees = true,
195
+ birds = 1,
136
196
  sunSize = 0.075,
137
197
  rays = 1,
138
198
  stars = 1,
@@ -158,6 +218,8 @@ const SunriseBackground = ({
158
218
  rays,
159
219
  stars,
160
220
  haze,
221
+ trees,
222
+ birds,
161
223
  speed,
162
224
  interactive,
163
225
  };
@@ -189,7 +251,9 @@ const SunriseBackground = ({
189
251
  uStars: { value: 1 },
190
252
  uHaze: { value: 1 },
191
253
  uSunSize: { value: 0.075 },
192
- uSunHeight: { value: 0.3 },
254
+ uSunHeight: { value: 0.28 },
255
+ uTrees: { value: 1 },
256
+ uBirds: { value: 1 },
193
257
  };
194
258
  const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
195
259
  const geometry = new PlaneGeometry(2, 2);
@@ -229,6 +293,8 @@ const SunriseBackground = ({
229
293
  u.uRays.value = o.rays;
230
294
  u.uStars.value = o.stars;
231
295
  u.uHaze.value = o.haze;
296
+ u.uTrees.value = o.trees ? 1 : 0;
297
+ u.uBirds.value = reduced ? 0 : Math.max(0, Math.min(1, o.birds));
232
298
  u.uSunSize.value = o.sunSize;
233
299
  u.uSunHeight.value = o.sunHeight;
234
300
  renderer.render(scene, camera);
@@ -1,15 +1,18 @@
1
- import React, { useRef } from "react";
1
+ import React, { useEffect, useRef } from "react";
2
2
  import { useAnimationFrame, useReducedMotion } from "framer-motion";
3
3
 
4
4
  /*
5
5
  * 3D Text Ring: letters stand on a ring in real 3D space and the ring
6
6
  * turns, so text sweeps around the back and comes forward again. Letters
7
- * fade and soften as they pass behind. Drag to spin it yourself.
7
+ * fade and soften as they pass behind. Drag to spin it yourself. On narrow
8
+ * screens the whole ring scales down to fit its container.
8
9
  */
9
10
 
10
11
  const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, tilt = -14, fontSize = 2, color = "currentColor", accentColor = "#a3e635", className = "" }) => {
11
12
  const reduced = useReducedMotion();
13
+ const box = useRef(null);
12
14
  const ring = useRef(null);
15
+ const fit = useRef(1);
13
16
  const letters = useRef([]);
14
17
  const angle = useRef(0);
15
18
  const vel = useRef(0);
@@ -17,13 +20,24 @@ const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, t
17
20
  const chars = Array.from(text);
18
21
  const step = 360 / chars.length;
19
22
 
23
+ useEffect(() => {
24
+ const el = box.current;
25
+ const ro = new ResizeObserver(() => {
26
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
27
+ const want = radius * 2.4 * rem;
28
+ fit.current = Math.min(1, el.getBoundingClientRect().width / want);
29
+ });
30
+ ro.observe(el);
31
+ return () => ro.disconnect();
32
+ }, [radius]);
33
+
20
34
  useAnimationFrame((_, delta) => {
21
35
  const dt = Math.min(0.05, delta / 1000);
22
36
  if (!drag.current) {
23
37
  vel.current *= 0.95;
24
38
  angle.current += (reduced ? 0 : speed) * dt + vel.current * dt;
25
39
  }
26
- if (ring.current) ring.current.style.transform = `rotateX(${tilt}deg) rotateY(${-angle.current}deg)`;
40
+ if (ring.current) ring.current.style.transform = `scale(${fit.current}) rotateX(${tilt}deg) rotateY(${-angle.current}deg)`;
27
41
  letters.current.forEach((el, i) => {
28
42
  if (!el) return;
29
43
  // How much this letter faces the viewer (1 front, -1 back).
@@ -35,6 +49,7 @@ const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, t
35
49
 
36
50
  return (
37
51
  <div
52
+ ref={box}
38
53
  className={`grid cursor-grab touch-none select-none place-items-center active:cursor-grabbing [perspective:900px] ${className}`}
39
54
  style={{ width: `${radius * 2.4}rem`, maxWidth: "100%", height: `${fontSize * 3.2 + 4}rem`, color }}
40
55
  onPointerDown={(e) => {
@@ -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={{ opacity: 0, y: top ? -30 : 30, scale: 0.94 }}
84
- animate={{ opacity: fromEnd > 2 && !hovered ? 0 : 1, y: top ? offset : -offset, scale: hovered ? 1 : 1 - fromEnd * 0.05 }}
85
- exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.18 } }}
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
- <div className={`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"}`}>
91
- <Icon className="mt-0.5 h-5 w-5 shrink-0" style={{ color }} />
92
- <div className="min-w-0">
93
- <p className="text-[0.88rem] font-semibold">{t.title}</p>
94
- {t.description && <p className={`mt-0.5 text-[0.8rem] ${dark ? "text-white/60" : "text-neutral-500"}`}>{t.description}</p>}
95
- </div>
96
- <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"}`}>
97
- <X className="h-3.5 w-3.5" />
98
- </button>
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
  );
package/dist/cli/index.js CHANGED
@@ -131,6 +131,15 @@ const componentMap = {
131
131
  "scroll-stack": "ScrollStack",
132
132
  "sunrise-background": "SunriseBackground",
133
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",
142
+ "liquid-glass": "LiquidGlass",
134
143
  "water-drop-reveal": "MaskView",
135
144
  };
136
145
  // Packages only some components need, installed on top of the shared ones.
@@ -140,6 +149,7 @@ const extraDeps = {
140
149
  SmokeBackground: ["ogl"],
141
150
  LiquidGradient: ["ogl"],
142
151
  WebGLCarousel: ["ogl"],
152
+ CrossedMarquee: ["simple-icons"],
143
153
  };
144
154
  async function main() {
145
155
  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.7.0",
3
+ "version": "2.9.0",
4
4
  "license": "MIT",
5
- "description": "107 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
5
+ "description": "116 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",