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.
- package/ReadMe.md +21 -9
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +29 -11
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +0 -1
- 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/FlipClock/FlipClock.jsx +3 -2
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +25 -18
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +273 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +1 -1
- 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/LiquidGlass/LiquidGlass.jsx +444 -0
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
- package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +20 -4
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +3 -3
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +68 -2
- 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/cli/index.js +10 -0
- package/package.json +2 -2
package/ReadMe.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/apex-ui-kit)
|
|
5
5
|
[](./Licence.md)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**116 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
|
|
8
8
|
|
|
9
9
|
Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL where a component needs them.
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL wh
|
|
|
15
15
|
|
|
16
16
|
## Why ApexUI
|
|
17
17
|
|
|
18
|
-
- **
|
|
18
|
+
- **116 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
|
|
19
19
|
- **You own the code.** The CLI copies plain `.jsx` files into your project, no runtime package to keep in sync.
|
|
20
20
|
- **Every docs page has** a live preview, a playground to tweak props, a Desktop / Tablet / Mobile preview, the full props table and a ready prompt you can paste into ChatGPT or Claude.
|
|
21
21
|
- **Smooth on real devices.** Animations stick to transform and opacity, respect reduced motion, and work with mouse, touch and keyboard.
|
|
@@ -99,12 +99,13 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
99
99
|
|
|
100
100
|
## Components
|
|
101
101
|
|
|
102
|
-
### Backgrounds (
|
|
102
|
+
### Backgrounds (17)
|
|
103
103
|
|
|
104
104
|
| Component | CLI name |
|
|
105
105
|
| --- | --- |
|
|
106
106
|
| [Aurora Background](https://apex-ui.in/components/aurora-background) | `aurora-background` |
|
|
107
107
|
| [BluePrint Background](https://apex-ui.in/components/blueprint-background) | `blueprint-background` |
|
|
108
|
+
| [Dither Waves](https://apex-ui.in/components/dither-waves) | `dither-waves` |
|
|
108
109
|
| [Dot Wave Background](https://apex-ui.in/components/dot-wave-background) | `dot-wave-background` |
|
|
109
110
|
| [Galaxy 3D](https://apex-ui.in/components/galaxy-3d) | `galaxy-3d` |
|
|
110
111
|
| [Horizon Glow](https://apex-ui.in/components/horizon-glow) | `horizon-glow` |
|
|
@@ -120,7 +121,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
120
121
|
| [Sunrise Background](https://apex-ui.in/components/sunrise-background) | `sunrise-background` |
|
|
121
122
|
| [Warp Tunnel](https://apex-ui.in/components/warp-tunnel) | `warp-tunnel` |
|
|
122
123
|
|
|
123
|
-
### Cards (
|
|
124
|
+
### Cards (20)
|
|
124
125
|
|
|
125
126
|
| Component | CLI name |
|
|
126
127
|
| --- | --- |
|
|
@@ -133,6 +134,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
133
134
|
| [Glare Card](https://apex-ui.in/components/glare-card) | `glare-card` |
|
|
134
135
|
| [Holo Card](https://apex-ui.in/components/holo-card) | `holo-card` |
|
|
135
136
|
| [Hyper Card](https://apex-ui.in/components/hyper-card) | `hyper-card` |
|
|
137
|
+
| [Image Accordion](https://apex-ui.in/components/image-accordion) | `image-accordion` |
|
|
136
138
|
| [Iso Stack](https://apex-ui.in/components/iso-stack) | `iso-stack` |
|
|
137
139
|
| [Parallax Card](https://apex-ui.in/components/parallax-card) | `parallax-card` |
|
|
138
140
|
| [Pixel Transition](https://apex-ui.in/components/pixel-transition) | `pixel-transition` |
|
|
@@ -144,7 +146,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
144
146
|
| [Swipe Cards](https://apex-ui.in/components/swipe-cards) | `swipe-cards` |
|
|
145
147
|
| [Testimonials](https://apex-ui.in/components/testimonials) | `testimonials` |
|
|
146
148
|
|
|
147
|
-
### Text & Motion (
|
|
149
|
+
### Text & Motion (20)
|
|
148
150
|
|
|
149
151
|
| Component | CLI name |
|
|
150
152
|
| --- | --- |
|
|
@@ -153,6 +155,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
153
155
|
| [Beam Bar](https://apex-ui.in/components/beam-bar) | `beam-bar` |
|
|
154
156
|
| [Blur Reveal Text](https://apex-ui.in/components/blur-reveal-text) | `blur-reveal-text` |
|
|
155
157
|
| [Circular Text](https://apex-ui.in/components/circular-text) | `circular-text` |
|
|
158
|
+
| [Crossed Marquee](https://apex-ui.in/components/crossed-marquee) | `crossed-marquee` |
|
|
156
159
|
| [Extruded Text](https://apex-ui.in/components/extruded-text) | `extruded-text` |
|
|
157
160
|
| [Flip Clock](https://apex-ui.in/components/flip-clock) | `flip-clock` |
|
|
158
161
|
| [Glitch Text](https://apex-ui.in/components/glitch-text) | `glitch-text` |
|
|
@@ -168,11 +171,12 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
168
171
|
| [True Focus](https://apex-ui.in/components/true-focus) | `true-focus` |
|
|
169
172
|
| [Typewriter Text](https://apex-ui.in/components/typewriter-text) | `typewriter-text` |
|
|
170
173
|
|
|
171
|
-
### Navigation (
|
|
174
|
+
### Navigation (11)
|
|
172
175
|
|
|
173
176
|
| Component | CLI name |
|
|
174
177
|
| --- | --- |
|
|
175
178
|
| [Animated Tabs](https://apex-ui.in/components/animated-tabs) | `animated-tabs` |
|
|
179
|
+
| [Command Palette](https://apex-ui.in/components/command-palette) | `command-palette` |
|
|
176
180
|
| [Floating Navbar](https://apex-ui.in/components/floating-navbar) | `floating-navbar` |
|
|
177
181
|
| [Gooey Nav](https://apex-ui.in/components/gooey-nav) | `gooey-nav` |
|
|
178
182
|
| [Magnify Dock](https://apex-ui.in/components/magnify-dock) | `magnify-dock` |
|
|
@@ -180,6 +184,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
180
184
|
| [Nav Menu](https://apex-ui.in/components/nav-menu) | `nav-menu` |
|
|
181
185
|
| [Radial Menu](https://apex-ui.in/components/radial-menu) | `radial-menu` |
|
|
182
186
|
| [Sidebar Nav](https://apex-ui.in/components/sidebar-nav) | `sidebar-nav` |
|
|
187
|
+
| [Theme Switcher](https://apex-ui.in/components/theme-switcher) | `theme-switcher` |
|
|
183
188
|
| [Theme Toggle](https://apex-ui.in/components/theme-toggle) | `theme-toggle` |
|
|
184
189
|
|
|
185
190
|
### Buttons (4)
|
|
@@ -191,7 +196,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
191
196
|
| [Magnetic Button](https://apex-ui.in/components/magnetic-button) | `magnetic-button` |
|
|
192
197
|
| [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
|
|
193
198
|
|
|
194
|
-
### Interactive (
|
|
199
|
+
### Interactive (31)
|
|
195
200
|
|
|
196
201
|
| Component | CLI name |
|
|
197
202
|
| --- | --- |
|
|
@@ -210,11 +215,15 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
210
215
|
| [Elastic Slider](https://apex-ui.in/components/elastic-slider) | `elastic-slider` |
|
|
211
216
|
| [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
|
|
212
217
|
| [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
|
|
218
|
+
| [Glass Lens](https://apex-ui.in/components/glass-lens) | `glass-lens` |
|
|
213
219
|
| [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
|
|
220
|
+
| [Infinite Canvas](https://apex-ui.in/components/infinite-canvas) | `infinite-canvas` |
|
|
214
221
|
| [Liquid Blob 3D](https://apex-ui.in/components/liquid-blob-3d) | `liquid-blob-3d` |
|
|
222
|
+
| [Liquid Glass](https://apex-ui.in/components/liquid-glass) | `liquid-glass` |
|
|
215
223
|
| [Magnet Lines](https://apex-ui.in/components/magnet-lines) | `magnet-lines` |
|
|
216
224
|
| [Magnetic Cursor](https://apex-ui.in/components/magnetic-cursor) | `magnetic-cursor` |
|
|
217
225
|
| [Marquee 3D](https://apex-ui.in/components/marquee-3d) | `marquee-3d` |
|
|
226
|
+
| [Orbit Icons](https://apex-ui.in/components/orbit-icons) | `orbit-icons` |
|
|
218
227
|
| [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
|
|
219
228
|
| [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
|
|
220
229
|
| [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
|
|
@@ -251,9 +260,12 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
251
260
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
252
261
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
253
262
|
|
|
254
|
-
## What's new in 2.
|
|
263
|
+
## What's new in 2.9
|
|
255
264
|
|
|
256
|
-
-
|
|
265
|
+
- New in 2.9: Liquid Glass (iOS style WebGL glass), 10 live page templates on the site, Arc Gallery auto scroll, Scroll Stack on the page scroll, Sunrise trees and birds.
|
|
266
|
+
- New in 2.8: Glass Lens (WebGL liquid glass), Dither Waves (WebGL 1-bit background), Infinite Canvas, Image Accordion, Orbit Icons, Command Palette, Crossed Marquee (tech-stack bands with simple-icons) and Theme Switcher (8 themes with a circle reveal).
|
|
267
|
+
- Toast Stack and Radial Menu no longer blink in production builds; Radial Menu, 3D Text Ring, Animated Tabs and Gooey Nav now fit 320px phones.
|
|
268
|
+
- 38 new components since 2.6, including WebGL and three.js (Sunrise Background, Horizon Glow, Circular Carousel, Arc Gallery, Galaxy 3D, Warp Tunnel, Wireframe Knot 3D, Marquee 3D, Distortion Image, Liquid Blob 3D, Particle Morph, Retro Grid 3D), Scroll Stack, Particle Text, Edge Blur, Magnetic Button, Radial Menu, 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.
|
|
257
269
|
- Accordion Marquee is rebuilt: one liquid band glides between rows on springs, leans toward the pointer, and its marquee speeds up with sideways mouse moves. No GSAP needed any more.
|
|
258
270
|
- AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
|
|
259
271
|
- 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.
|
|
@@ -58,7 +58,7 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
|
|
|
58
58
|
aria-controls={`${id}-panel`}
|
|
59
59
|
tabIndex={on ? 0 : -1}
|
|
60
60
|
onClick={() => select(i)}
|
|
61
|
-
className={`relative flex-1 whitespace-nowrap px-3 py-2 text-[0.82rem] sm:px-4 sm:text-[0.88rem] font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
|
|
61
|
+
className={`relative flex-1 whitespace-nowrap px-3 py-2 text-[0.82rem] max-[380px]:px-2 max-[380px]:text-[0.76rem] sm:px-4 sm:text-[0.88rem] font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
|
|
62
62
|
>
|
|
63
63
|
{on && (
|
|
64
64
|
<motion.span
|
|
@@ -3,9 +3,10 @@ import { Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBCol
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* Arc Gallery: an endless row of image cards laid along a gentle arc and
|
|
6
|
-
* drawn in WebGL.
|
|
7
|
-
*
|
|
8
|
-
* the edges fade out, and a title sits under
|
|
6
|
+
* drawn in WebGL. It drifts on its own (pausing on hover); drag, flick or
|
|
7
|
+
* swipe sideways to move it and it eases to a stop on a card. While it
|
|
8
|
+
* moves the cards ripple and bow, the edges fade out, and a title sits under
|
|
9
|
+
* each card. Arrow keys step. Vertical wheel scrolling is left to the page.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
|
|
@@ -54,13 +55,13 @@ void main(){
|
|
|
54
55
|
gl_FragColor = vec4(col, edge * f * uReady);
|
|
55
56
|
}`;
|
|
56
57
|
|
|
57
|
-
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
|
+
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, autoScroll = 40, pauseOnHover = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
59
|
const wrap = useRef(null);
|
|
59
60
|
const labels = useRef([]);
|
|
60
61
|
const api = useRef(null);
|
|
61
62
|
const [active, setActive] = useState(0);
|
|
62
63
|
const live = useRef({});
|
|
63
|
-
live.current = { bend, wave, pull, fadeEdges, ease, snap, onChange };
|
|
64
|
+
live.current = { bend, wave, pull, fadeEdges, ease, snap, autoScroll, pauseOnHover, onChange };
|
|
64
65
|
const list = items.length ? items : DEFAULT_ITEMS;
|
|
65
66
|
const key = list.map((i) => i.image).join("|");
|
|
66
67
|
|
|
@@ -84,7 +85,6 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
84
85
|
const textures = list.map((item) => {
|
|
85
86
|
const entry = { tex: null, aspect: 1, at: 0 };
|
|
86
87
|
loader.load(item.image, (t) => {
|
|
87
|
-
t.colorSpace = SRGBColorSpace;
|
|
88
88
|
entry.tex = t;
|
|
89
89
|
entry.aspect = t.image.width / t.image.height;
|
|
90
90
|
entry.at = performance.now();
|
|
@@ -116,7 +116,8 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
116
116
|
return { mesh, m };
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
-
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0 };
|
|
119
|
+
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0, hover: false, hold: 0 };
|
|
120
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
120
121
|
let cardW = 1;
|
|
121
122
|
let cardH = 1;
|
|
122
123
|
let spacing = 1;
|
|
@@ -172,6 +173,10 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
172
173
|
const o = live.current;
|
|
173
174
|
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
174
175
|
last = now;
|
|
176
|
+
// Auto scroll drifts the row along, pausing while hovered, dragged or
|
|
177
|
+
// just after someone moved it by hand.
|
|
178
|
+
const auto = o.autoScroll && !reduced && !s.drag && !(o.pauseOnHover && s.hover) && now > s.hold;
|
|
179
|
+
if (auto) s.target += o.autoScroll * dt;
|
|
175
180
|
const before = s.scroll;
|
|
176
181
|
const follow = 1 - Math.pow(1 - Math.min(1, Math.max(0.01, o.ease)), dt * 60);
|
|
177
182
|
s.scroll += (s.target - s.scroll) * follow;
|
|
@@ -185,7 +190,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
185
190
|
const pullGoal = Math.max(-1, Math.min(1, s.vel / (spacing * 7))) * 0.32 * o.pull;
|
|
186
191
|
s.pull += (pullGoal - s.pull) * (1 - Math.exp(-dt / 0.12));
|
|
187
192
|
if (Math.abs(s.pull) < 0.0005 && !pullGoal) s.pull = 0;
|
|
188
|
-
let busy = s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
193
|
+
let busy = auto || s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
189
194
|
|
|
190
195
|
const first = Math.floor((s.scroll - view.w / 2) / spacing) - 1;
|
|
191
196
|
slots.forEach((slot, k) => {
|
|
@@ -251,6 +256,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
251
256
|
const onDown = (e) => {
|
|
252
257
|
if (e.button !== 0) return;
|
|
253
258
|
s.drag = { x: e.clientX, start: s.target, moved: false, samples: [] };
|
|
259
|
+
wake();
|
|
254
260
|
};
|
|
255
261
|
const onMove = (e) => {
|
|
256
262
|
const g = s.drag;
|
|
@@ -269,6 +275,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
269
275
|
const onUp = (e) => {
|
|
270
276
|
const g = s.drag;
|
|
271
277
|
s.drag = null;
|
|
278
|
+
s.hold = performance.now() + 1800;
|
|
272
279
|
if (!g) return;
|
|
273
280
|
if (g.moved && g.samples.length > 1) {
|
|
274
281
|
const a = g.samples[0];
|
|
@@ -286,10 +293,12 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
286
293
|
settle();
|
|
287
294
|
};
|
|
288
295
|
const onWheel = (e) => {
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
// Only sideways wheel / trackpad swipes move the row; vertical
|
|
297
|
+
// scrolling belongs to the page.
|
|
298
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
291
299
|
e.preventDefault();
|
|
292
|
-
s.target +=
|
|
300
|
+
s.target += e.deltaX * 1.2;
|
|
301
|
+
s.hold = performance.now() + 1800;
|
|
293
302
|
clearTimeout(snapTimer);
|
|
294
303
|
snapTimer = setTimeout(settle, 140);
|
|
295
304
|
wake();
|
|
@@ -299,6 +308,13 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
299
308
|
canvas.addEventListener("pointerup", onUp);
|
|
300
309
|
canvas.addEventListener("pointercancel", onUp);
|
|
301
310
|
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
311
|
+
const onEnter = () => (s.hover = true);
|
|
312
|
+
const onLeave = () => {
|
|
313
|
+
s.hover = false;
|
|
314
|
+
wake();
|
|
315
|
+
};
|
|
316
|
+
canvas.addEventListener("pointerenter", onEnter);
|
|
317
|
+
canvas.addEventListener("pointerleave", onLeave);
|
|
302
318
|
const io = new IntersectionObserver(([entry]) => {
|
|
303
319
|
visible = entry.isIntersecting;
|
|
304
320
|
wake();
|
|
@@ -316,6 +332,8 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
316
332
|
canvas.removeEventListener("pointerup", onUp);
|
|
317
333
|
canvas.removeEventListener("pointercancel", onUp);
|
|
318
334
|
canvas.removeEventListener("wheel", onWheel);
|
|
335
|
+
canvas.removeEventListener("pointerenter", onEnter);
|
|
336
|
+
canvas.removeEventListener("pointerleave", onLeave);
|
|
319
337
|
textures.forEach((t) => t.tex?.dispose());
|
|
320
338
|
slots.forEach((slot) => slot.m.dispose());
|
|
321
339
|
geometry.dispose();
|
|
@@ -124,7 +124,6 @@ const CircularCarousel = ({
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
loader.load(item.src, (t) => {
|
|
127
|
-
t.colorSpace = SRGBColorSpace;
|
|
128
127
|
m.uniforms.uTex.value = t;
|
|
129
128
|
const ia = t.image.width / t.image.height;
|
|
130
129
|
// Cover crop: show the middle of the image at the card's shape.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { CornerDownLeft, FileText, Github, LayoutGrid, LogOut, Moon, Plus, Search, Settings, Sparkles, User } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Command Palette: a ⌘K / Ctrl+K search box for jumping anywhere. Typing
|
|
6
|
+
* filters with a forgiving fuzzy match (letters in order, gaps allowed) and
|
|
7
|
+
* highlights the matched letters; arrow keys move a highlight that slides
|
|
8
|
+
* between rows, Enter runs the command, Escape closes. The panel eases in and
|
|
9
|
+
* out with CSS transitions only, so it never blinks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_GROUPS = [
|
|
13
|
+
{
|
|
14
|
+
heading: "Suggestions",
|
|
15
|
+
items: [
|
|
16
|
+
{ id: "new", label: "Create new project", icon: Plus, shortcut: "N" },
|
|
17
|
+
{ id: "components", label: "Browse components", icon: LayoutGrid, hint: "107 ready to use" },
|
|
18
|
+
{ id: "ai", label: "Ask AI to build a section", icon: Sparkles, keywords: "assistant generate" },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
heading: "Pages",
|
|
23
|
+
items: [
|
|
24
|
+
{ id: "docs", label: "Documentation", icon: FileText },
|
|
25
|
+
{ id: "github", label: "Open GitHub", icon: Github, keywords: "repo source" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
heading: "Settings",
|
|
30
|
+
items: [
|
|
31
|
+
{ id: "profile", label: "Profile", icon: User, shortcut: "P" },
|
|
32
|
+
{ id: "theme", label: "Toggle dark mode", icon: Moon, keywords: "theme light" },
|
|
33
|
+
{ id: "settings", label: "Preferences", icon: Settings, shortcut: "," },
|
|
34
|
+
{ id: "logout", label: "Log out", icon: LogOut },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Letters in order, gaps allowed. Lower score is better; returns matched indexes.
|
|
40
|
+
function fuzzy(text, query) {
|
|
41
|
+
if (!query) return { score: 0, hits: [] };
|
|
42
|
+
const t = text.toLowerCase();
|
|
43
|
+
const q = query.toLowerCase();
|
|
44
|
+
let ti = 0;
|
|
45
|
+
let score = 0;
|
|
46
|
+
let prev = -1;
|
|
47
|
+
const hits = [];
|
|
48
|
+
for (const ch of q) {
|
|
49
|
+
if (ch === " ") continue;
|
|
50
|
+
const at = t.indexOf(ch, ti);
|
|
51
|
+
if (at < 0) return null;
|
|
52
|
+
score += at - prev - 1 + (at === 0 || t[at - 1] === " " ? 0 : 0.5);
|
|
53
|
+
hits.push(at);
|
|
54
|
+
prev = at;
|
|
55
|
+
ti = at + 1;
|
|
56
|
+
}
|
|
57
|
+
return { score, hits };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const Highlight = ({ text, hits }) => {
|
|
61
|
+
if (!hits.length) return text;
|
|
62
|
+
const set = new Set(hits);
|
|
63
|
+
return [...text].map((ch, i) => (set.has(i) ? <mark key={i} className="bg-transparent font-semibold text-[var(--cp-accent)]">{ch}</mark> : ch));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const CommandPalette = ({ groups = DEFAULT_GROUPS, placeholder = "Type a command or search…", hotkey = "k", accentColor = "#a3e635", theme = "dark", contained = false, triggerLabel = "Search", defaultOpen = false, onSelect, className = "" }) => {
|
|
67
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
68
|
+
const [mounted, setMounted] = useState(defaultOpen);
|
|
69
|
+
const [shown, setShown] = useState(defaultOpen);
|
|
70
|
+
const [query, setQuery] = useState("");
|
|
71
|
+
const [active, setActive] = useState(0);
|
|
72
|
+
const [done, setDone] = useState(null);
|
|
73
|
+
const input = useRef(null);
|
|
74
|
+
const listRef = useRef(null);
|
|
75
|
+
const bar = useRef(null);
|
|
76
|
+
const rows = useRef([]);
|
|
77
|
+
const dark = theme !== "light";
|
|
78
|
+
|
|
79
|
+
const results = useMemo(() => {
|
|
80
|
+
const out = [];
|
|
81
|
+
groups.forEach((g) => {
|
|
82
|
+
const items = g.items
|
|
83
|
+
.map((it) => {
|
|
84
|
+
const m = fuzzy(it.label, query) ?? (it.keywords && fuzzy(it.keywords, query) ? { score: 50, hits: [] } : null);
|
|
85
|
+
return m && { ...it, ...m };
|
|
86
|
+
})
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.sort((a, b) => a.score - b.score);
|
|
89
|
+
if (items.length) out.push({ heading: g.heading, items });
|
|
90
|
+
});
|
|
91
|
+
return out;
|
|
92
|
+
}, [groups, query]);
|
|
93
|
+
const flat = useMemo(() => results.flatMap((g) => g.items), [results]);
|
|
94
|
+
|
|
95
|
+
// Mount, then show on the next frame so the enter transition runs; hide, then unmount.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (open) {
|
|
98
|
+
setMounted(true);
|
|
99
|
+
const id = requestAnimationFrame(() => requestAnimationFrame(() => setShown(true)));
|
|
100
|
+
return () => cancelAnimationFrame(id);
|
|
101
|
+
}
|
|
102
|
+
setShown(false);
|
|
103
|
+
const t = setTimeout(() => setMounted(false), 220);
|
|
104
|
+
return () => clearTimeout(t);
|
|
105
|
+
}, [open]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (shown) input.current?.focus({ preventScroll: true });
|
|
109
|
+
}, [shown]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const onKey = (e) => {
|
|
113
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === hotkey) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
setOpen((o) => !o);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener("keydown", onKey);
|
|
119
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
120
|
+
}, [hotkey]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => setActive(0), [query]);
|
|
123
|
+
|
|
124
|
+
// Slide the highlight to the active row and keep it in view.
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
const row = rows.current[active];
|
|
127
|
+
const b = bar.current;
|
|
128
|
+
if (!row || !b) {
|
|
129
|
+
if (b) b.style.opacity = "0";
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
b.style.opacity = "1";
|
|
133
|
+
b.style.transform = `translateY(${row.offsetTop}px)`;
|
|
134
|
+
b.style.height = `${row.offsetHeight}px`;
|
|
135
|
+
row.scrollIntoView({ block: "nearest" });
|
|
136
|
+
}, [active, flat.length, mounted]);
|
|
137
|
+
|
|
138
|
+
const close = useCallback(() => {
|
|
139
|
+
setOpen(false);
|
|
140
|
+
setTimeout(() => setQuery(""), 220);
|
|
141
|
+
}, []);
|
|
142
|
+
const run = (item) => {
|
|
143
|
+
if (!item) return;
|
|
144
|
+
onSelect?.(item);
|
|
145
|
+
setDone(item.label);
|
|
146
|
+
setTimeout(() => setDone(null), 1600);
|
|
147
|
+
close();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const onKeyDown = (e) => {
|
|
151
|
+
if (e.key === "ArrowDown") setActive((a) => (flat.length ? (a + 1) % flat.length : 0));
|
|
152
|
+
else if (e.key === "ArrowUp") setActive((a) => (flat.length ? (a - 1 + flat.length) % flat.length : 0));
|
|
153
|
+
else if (e.key === "Home") setActive(0);
|
|
154
|
+
else if (e.key === "End") setActive(Math.max(0, flat.length - 1));
|
|
155
|
+
else if (e.key === "Enter") run(flat[active]);
|
|
156
|
+
else if (e.key === "Escape") close();
|
|
157
|
+
else return;
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const t = dark
|
|
162
|
+
? { panel: "border-white/10 bg-neutral-950/90 text-white", muted: "text-white/45", line: "border-white/10", row: "text-white/85", hl: "bg-white/[0.07]", kbd: "border-white/15 bg-white/5 text-white/60", trigger: "border-white/10 bg-neutral-900 text-white/70 hover:text-white" }
|
|
163
|
+
: { panel: "border-black/10 bg-white/95 text-neutral-900", muted: "text-neutral-400", line: "border-black/10", row: "text-neutral-700", hl: "bg-black/[0.05]", kbd: "border-black/10 bg-black/[0.03] text-neutral-500", trigger: "border-black/10 bg-white text-neutral-500 hover:text-neutral-900" };
|
|
164
|
+
let n = -1;
|
|
165
|
+
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className={className} style={{ "--cp-accent": accentColor }}>
|
|
169
|
+
<button type="button" onClick={() => setOpen(true)} className={`inline-flex w-full max-w-xs items-center gap-2.5 rounded-xl border px-3.5 py-2.5 text-sm transition-colors ${t.trigger}`}>
|
|
170
|
+
<Search className="h-4 w-4" />
|
|
171
|
+
<span className="flex-1 text-left">{triggerLabel}</span>
|
|
172
|
+
<kbd className={`rounded-md border px-1.5 py-0.5 font-mono text-[0.68rem] ${t.kbd}`}>{isMac ? "⌘" : "Ctrl"} {hotkey.toUpperCase()}</kbd>
|
|
173
|
+
</button>
|
|
174
|
+
{done && <p className={`mt-3 text-center text-xs ${t.muted}`}>Ran “{done}”</p>}
|
|
175
|
+
|
|
176
|
+
{mounted && (
|
|
177
|
+
<div className={`${contained ? "absolute" : "fixed"} inset-0 z-[300] flex items-start justify-center px-3 pt-[12vh]`} onPointerDown={(e) => e.target === e.currentTarget && close()}>
|
|
178
|
+
<div aria-hidden="true" className="absolute inset-0 bg-black/50 backdrop-blur-[2px] transition-opacity duration-200 ease-out" style={{ opacity: shown ? 1 : 0 }} onPointerDown={close} />
|
|
179
|
+
<div
|
|
180
|
+
role="dialog"
|
|
181
|
+
aria-modal="true"
|
|
182
|
+
aria-label="Command palette"
|
|
183
|
+
className={`relative w-full max-w-lg overflow-hidden rounded-2xl border shadow-[0_30px_80px_-20px_rgba(0,0,0,0.6)] backdrop-blur-xl ${t.panel}`}
|
|
184
|
+
style={{ opacity: shown ? 1 : 0, transform: shown ? "none" : "translateY(-10px) scale(0.97)", transition: "opacity 180ms ease-out, transform 260ms cubic-bezier(0.22, 1, 0.36, 1)" }}
|
|
185
|
+
>
|
|
186
|
+
<div className={`flex items-center gap-3 border-b px-4 ${t.line}`}>
|
|
187
|
+
<Search className={`h-4 w-4 shrink-0 ${t.muted}`} />
|
|
188
|
+
<input
|
|
189
|
+
ref={input}
|
|
190
|
+
value={query}
|
|
191
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
192
|
+
onKeyDown={onKeyDown}
|
|
193
|
+
placeholder={placeholder}
|
|
194
|
+
role="combobox"
|
|
195
|
+
aria-expanded="true"
|
|
196
|
+
aria-controls="cp-list"
|
|
197
|
+
aria-activedescendant={flat[active] ? `cp-${flat[active].id}` : undefined}
|
|
198
|
+
className="h-12 w-full bg-transparent text-[0.95rem] outline-none placeholder:opacity-50"
|
|
199
|
+
/>
|
|
200
|
+
<kbd className={`hidden rounded-md border px-1.5 py-0.5 font-mono text-[0.65rem] sm:block ${t.kbd}`}>ESC</kbd>
|
|
201
|
+
</div>
|
|
202
|
+
<div ref={listRef} id="cp-list" role="listbox" className="relative max-h-[min(22rem,55vh)] overflow-y-auto overscroll-contain p-2 [scrollbar-width:thin]">
|
|
203
|
+
<div ref={bar} aria-hidden="true" className={`pointer-events-none absolute inset-x-2 top-0 rounded-xl ${t.hl}`} style={{ transition: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1), height 180ms ease, opacity 120ms" }}>
|
|
204
|
+
<span className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-full bg-[var(--cp-accent)]" />
|
|
205
|
+
</div>
|
|
206
|
+
{results.length === 0 && <p className={`px-3 py-10 text-center text-sm ${t.muted}`}>No results for “{query}”</p>}
|
|
207
|
+
{results.map((g) => (
|
|
208
|
+
<div key={g.heading} role="group" aria-label={g.heading}>
|
|
209
|
+
<p className={`px-3 pb-1 pt-2.5 text-[0.68rem] font-semibold uppercase tracking-[0.14em] ${t.muted}`}>{g.heading}</p>
|
|
210
|
+
{g.items.map((it) => {
|
|
211
|
+
n += 1;
|
|
212
|
+
const i = n;
|
|
213
|
+
const Icon = it.icon;
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
key={it.id}
|
|
217
|
+
id={`cp-${it.id}`}
|
|
218
|
+
ref={(e) => (rows.current[i] = e)}
|
|
219
|
+
role="option"
|
|
220
|
+
aria-selected={i === active}
|
|
221
|
+
onPointerMove={() => i !== active && setActive(i)}
|
|
222
|
+
onClick={() => run(it)}
|
|
223
|
+
className={`relative flex cursor-pointer items-center gap-3 rounded-xl px-3 py-2.5 text-sm ${t.row}`}
|
|
224
|
+
>
|
|
225
|
+
{Icon && <Icon className={`h-4 w-4 shrink-0 transition-colors ${i === active ? "text-[var(--cp-accent)]" : t.muted}`} />}
|
|
226
|
+
<span className="flex-1 truncate">
|
|
227
|
+
<Highlight text={it.label} hits={it.hits} />
|
|
228
|
+
{it.hint && <span className={`ml-2 text-xs ${t.muted}`}>{it.hint}</span>}
|
|
229
|
+
</span>
|
|
230
|
+
{it.shortcut && <kbd className={`rounded-md border px-1.5 py-0.5 font-mono text-[0.65rem] ${t.kbd}`}>{isMac ? "⌘" : "Ctrl"} {it.shortcut}</kbd>}
|
|
231
|
+
{i === active && <CornerDownLeft className={`h-3.5 w-3.5 ${t.muted}`} />}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
<div className={`flex items-center gap-4 border-t px-4 py-2.5 text-[0.7rem] ${t.line} ${t.muted}`}>
|
|
239
|
+
<span>↑↓ to move</span>
|
|
240
|
+
<span>↵ to run</span>
|
|
241
|
+
<span className="ml-auto">esc to close</span>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export default CommandPalette;
|