Skip to content

Commit df6ed42

Browse files
committed
add v3 docs
1 parent 65ccdcb commit df6ed42

File tree

79 files changed

+2094
-416
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2094
-416
lines changed

components/info.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ReactNode } from 'react';
2+
3+
interface InfoProps {
4+
children: ReactNode;
5+
title?: string;
6+
}
7+
8+
/**
9+
* A Vue-style info component for documentation.
10+
* Displays an info callout with a blue accent, similar to VitePress/VuePress info blocks.
11+
*/
12+
export function Info({ children, title = 'INFO' }: InfoProps) {
13+
return (
14+
<div className="my-4 rounded-lg border border-blue-200 bg-blue-50 dark:border-blue-900/50 dark:bg-blue-950/30">
15+
<div className="flex items-start gap-3 px-4 pb-4 pt-2">
16+
<div className="flex-1">
17+
<p className="mb-2 text-sm font-semibold uppercase text-blue-700 dark:text-blue-400">
18+
{title}
19+
</p>
20+
<div className="text-sm text-blue-800 dark:text-blue-200 [&>p]:m-0 [&>p:not(:last-child)]:mb-2">
21+
{children}
22+
</div>
23+
</div>
24+
</div>
25+
</div>
26+
);
27+
}
28+

components/screenshots-gallery.tsx

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
'use client';
2+
3+
import { useState, useEffect, useCallback, useMemo } from 'react';
4+
5+
interface ScreenshotsGalleryProps {
6+
images: Record<string, string>;
7+
className?: string;
8+
gridClass?: string;
9+
}
10+
11+
export function ScreenshotsGallery({ images, className, gridClass }: ScreenshotsGalleryProps) {
12+
const [lightboxOpen, setLightboxOpen] = useState(false);
13+
const [currentImage, setCurrentImage] = useState('');
14+
const [currentTitle, setCurrentTitle] = useState('');
15+
const [currentIndex, setCurrentIndex] = useState(0);
16+
17+
const imageKeys = useMemo(() => Object.keys(images), [images]);
18+
19+
const openLightbox = useCallback((imageUrl: string, title: string) => {
20+
setCurrentImage(imageUrl);
21+
setCurrentTitle(title);
22+
setCurrentIndex(imageKeys.indexOf(title));
23+
setLightboxOpen(true);
24+
document.body.style.overflow = 'hidden';
25+
}, [imageKeys]);
26+
27+
const closeLightbox = useCallback(() => {
28+
setLightboxOpen(false);
29+
document.body.style.overflow = '';
30+
}, []);
31+
32+
const nextImage = useCallback(() => {
33+
const newIndex = (currentIndex + 1) % imageKeys.length;
34+
const key = imageKeys[newIndex];
35+
setCurrentIndex(newIndex);
36+
setCurrentImage(images[key]);
37+
setCurrentTitle(key);
38+
}, [currentIndex, imageKeys, images]);
39+
40+
const previousImage = useCallback(() => {
41+
const newIndex = (currentIndex - 1 + imageKeys.length) % imageKeys.length;
42+
const key = imageKeys[newIndex];
43+
setCurrentIndex(newIndex);
44+
setCurrentImage(images[key]);
45+
setCurrentTitle(key);
46+
}, [currentIndex, imageKeys, images]);
47+
48+
useEffect(() => {
49+
const handleKeydown = (e: KeyboardEvent) => {
50+
if (!lightboxOpen) return;
51+
52+
switch (e.key) {
53+
case 'Escape':
54+
closeLightbox();
55+
break;
56+
case 'ArrowRight':
57+
if (imageKeys.length > 1) {
58+
nextImage();
59+
}
60+
break;
61+
case 'ArrowLeft':
62+
if (imageKeys.length > 1) {
63+
previousImage();
64+
}
65+
break;
66+
}
67+
};
68+
69+
if (lightboxOpen) {
70+
document.addEventListener('keydown', handleKeydown);
71+
}
72+
73+
return () => {
74+
document.removeEventListener('keydown', handleKeydown);
75+
};
76+
}, [lightboxOpen, closeLightbox, nextImage, previousImage, imageKeys.length]);
77+
78+
return (
79+
<div className={className ?? "not-prose my-16"}>
80+
{/* Gallery Grid */}
81+
<div className={gridClass || 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8'}>
82+
{Object.entries(images).map(([title, imageUrl]) => (
83+
<div
84+
key={title}
85+
onClick={() => openLightbox(imageUrl, title)}
86+
className="group relative overflow-hidden rounded-xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02] cursor-pointer bg-white dark:bg-gray-800"
87+
>
88+
{/* Image Container with aspect ratio for 2048x2158 */}
89+
<div className="relative aspect-[2048/2158] overflow-hidden">
90+
{/* Gradient overlay on hover */}
91+
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10"></div>
92+
93+
{/* Image */}
94+
<img
95+
src={imageUrl}
96+
alt={title}
97+
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
98+
loading="lazy"
99+
/>
100+
101+
{/* Title overlay */}
102+
<div className="absolute bottom-0 left-0 right-0 p-6 z-20 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
103+
<h3 className="text-white text-lg font-semibold capitalize">{title}</h3>
104+
<p className="text-gray-200 text-sm mt-1">Click to view full size</p>
105+
</div>
106+
107+
{/* Zoom icon */}
108+
<div className="absolute top-4 right-4 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
109+
<div className="bg-white/90 dark:bg-gray-900/90 rounded-full p-2 shadow-lg">
110+
<svg className="w-5 h-5 text-gray-900 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
111+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path>
112+
</svg>
113+
</div>
114+
</div>
115+
</div>
116+
</div>
117+
))}
118+
</div>
119+
120+
{/* Lightbox Modal */}
121+
{lightboxOpen && (
122+
<div
123+
onClick={closeLightbox}
124+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 backdrop-blur-sm p-4"
125+
>
126+
{/* Close button */}
127+
<button
128+
onClick={closeLightbox}
129+
className="absolute top-4 right-4 z-50 text-white hover:text-gray-300 transition-colors p-2 hover:bg-white/10 rounded-full"
130+
>
131+
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
132+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
133+
</svg>
134+
</button>
135+
136+
{/* Image title */}
137+
<div className="absolute top-4 left-4 z-50 bg-black/50 backdrop-blur-sm px-4 py-2 rounded-lg">
138+
<h3 className="text-white text-xl font-semibold capitalize">{currentTitle}</h3>
139+
</div>
140+
141+
{/* Image container */}
142+
<div
143+
onClick={(e) => e.stopPropagation()}
144+
className="relative max-w-6xl max-h-[90vh] flex items-center justify-center"
145+
>
146+
<img
147+
src={currentImage}
148+
alt={currentTitle}
149+
className="max-w-full max-h-[90vh] object-contain rounded-lg shadow-2xl"
150+
/>
151+
</div>
152+
153+
{/* Navigation arrows (if multiple images) */}
154+
{imageKeys.length > 1 && (
155+
<button
156+
onClick={(e) => {
157+
e.stopPropagation();
158+
previousImage();
159+
}}
160+
className="absolute left-4 top-1/2 -translate-y-1/2 text-white hover:text-gray-300 transition-colors p-3 hover:bg-white/10 rounded-full"
161+
>
162+
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
163+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7"></path>
164+
</svg>
165+
</button>
166+
)}
167+
168+
{imageKeys.length > 1 && (
169+
<button
170+
onClick={(e) => {
171+
e.stopPropagation();
172+
nextImage();
173+
}}
174+
className="absolute right-4 top-1/2 -translate-y-1/2 text-white hover:text-gray-300 transition-colors p-3 hover:bg-white/10 rounded-full"
175+
>
176+
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
177+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
178+
</svg>
179+
</button>
180+
)}
181+
182+
{/* Image counter */}
183+
{imageKeys.length > 1 && (
184+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 backdrop-blur-sm px-4 py-2 rounded-lg">
185+
<p className="text-white text-sm">{currentIndex + 1} / {imageKeys.length}</p>
186+
</div>
187+
)}
188+
</div>
189+
)}
190+
</div>
191+
);
192+
}

components/tip.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ReactNode } from 'react';
2+
3+
interface TipProps {
4+
children: ReactNode;
5+
title?: string;
6+
}
7+
8+
/**
9+
* A Vue-style tip component for documentation.
10+
* Displays a tip callout with a green accent, similar to VitePress/VuePress tip blocks.
11+
*/
12+
export function Tip({ children, title = 'TIP' }: TipProps) {
13+
return (
14+
<div className="my-4 rounded-lg border border-green-200 bg-green-50 dark:border-green-900/50 dark:bg-green-950/30">
15+
<div className="flex items-start gap-3 px-4 pb-4 pt-2">
16+
<div className="flex-1">
17+
<p className="mb-2 text-sm font-semibold uppercase text-green-700 dark:text-green-400">
18+
{title}
19+
</p>
20+
<div className="text-sm text-green-800 dark:text-green-200 [&>p]:m-0 [&>p:not(:last-child)]:mb-2">
21+
{children}
22+
</div>
23+
</div>
24+
</div>
25+
</div>
26+
);
27+
}
28+

components/youtube.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
interface YouTubeProps {
2+
id: string;
3+
title?: string;
4+
}
5+
6+
/**
7+
* A YouTube embed component for documentation.
8+
* Uses lite-youtube-embed style with a thumbnail preview.
9+
*/
10+
export function YouTube({ id, title = 'YouTube Video' }: YouTubeProps) {
11+
return (
12+
<div className="relative my-4 aspect-video w-full overflow-hidden rounded-lg">
13+
<iframe
14+
className="absolute inset-0 h-full w-full"
15+
src={`https://www.youtube.com/embed/${id}`}
16+
title={title}
17+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
18+
allowFullScreen
19+
/>
20+
</div>
21+
);
22+
}
23+

0 commit comments

Comments
 (0)