@docubook/create 2.4.0 → 2.5.1
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/package.json +1 -1
- package/src/dist/app/docs/[[...slug]]/page.tsx +55 -59
- package/src/dist/app/docs/layout.tsx +11 -4
- package/src/dist/app/layout.tsx +10 -7
- package/src/dist/app/page.tsx +1 -1
- package/src/dist/components/{context-popover.tsx → ContextPopover.tsx} +3 -2
- package/src/dist/components/DocSearch.tsx +16 -15
- package/src/dist/components/{docs-breadcrumb.tsx → DocsBreadcrumb.tsx} +1 -1
- package/src/dist/components/DocsNavbar.tsx +46 -0
- package/src/dist/components/DocsSidebar.tsx +207 -0
- package/src/dist/components/Github.tsx +26 -0
- package/src/dist/components/{scroll-to-top.tsx → ScrollToTop.tsx} +16 -9
- package/src/dist/components/SearchBox.tsx +39 -0
- package/src/dist/components/SearchContext.tsx +47 -0
- package/src/dist/components/SearchModal.tsx +77 -79
- package/src/dist/components/SearchTrigger.tsx +20 -15
- package/src/dist/components/Sponsor.tsx +2 -2
- package/src/dist/components/{theme-toggle.tsx → ThemeToggle.tsx} +10 -10
- package/src/dist/components/TocObserver.tsx +197 -0
- package/src/dist/components/footer.tsx +16 -12
- package/src/dist/components/leftbar.tsx +45 -73
- package/src/dist/components/markdown/AccordionContext.tsx +21 -0
- package/src/dist/components/markdown/AccordionGroupMdx.tsx +11 -22
- package/src/dist/components/markdown/AccordionMdx.tsx +58 -59
- package/src/dist/components/markdown/PreMdx.tsx +2 -2
- package/src/dist/components/navbar.tsx +130 -53
- package/src/dist/components/toc.tsx +16 -14
- package/src/dist/components/typography.tsx +1 -1
- package/src/dist/components/ui/icon-cloud.tsx +353 -0
- package/src/dist/components/ui/scroll-area.tsx +2 -2
- package/src/dist/components/ui/sheet.tsx +4 -4
- package/src/dist/components/ui/toggle.tsx +3 -3
- package/src/dist/docs/components/accordion-group.mdx +13 -12
- package/src/dist/docs/components/accordion.mdx +11 -14
- package/src/dist/docu.json +3 -0
- package/src/dist/hooks/useActiveSection.ts +45 -33
- package/src/dist/hooks/useScrollPosition.ts +16 -14
- package/src/dist/lib/search/algolia.ts +5 -0
- package/src/dist/lib/search/built-in.ts +43 -0
- package/src/dist/lib/search/config.ts +7 -0
- package/src/dist/lib/toc.ts +1 -0
- package/src/dist/lib/utils.ts +13 -54
- package/src/dist/package.json +1 -1
- package/src/dist/styles/algolia.css +12 -35
- package/src/dist/styles/{syntax.css → override.css} +82 -39
- package/src/dist/tailwind.config.ts +11 -110
- package/src/dist/components/AccordionContext.ts +0 -4
- package/src/dist/components/GithubStart.tsx +0 -44
- package/src/dist/components/mob-toc.tsx +0 -134
- package/src/dist/components/search.tsx +0 -55
- package/src/dist/components/toc-observer.tsx +0 -254
- /package/src/dist/components/{docs-menu.tsx → DocsMenu.tsx} +0 -0
- /package/src/dist/components/{edit-on-github.tsx → EditWithGithub.tsx} +0 -0
- /package/src/dist/components/{theme-provider.tsx → ThemeProvider.tsx} +0 -0
|
@@ -1,87 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
import Image from "next/image";
|
|
4
|
-
import Search from "@/components/search";
|
|
5
|
-
import Anchor from "@/components/anchor";
|
|
6
|
-
import { SheetLeftbar } from "@/components/leftbar";
|
|
7
|
-
import { SheetClose } from "@/components/ui/sheet";
|
|
8
|
-
import { Separator } from "@/components/ui/separator";
|
|
9
|
-
import docuConfig from "@/docu.json"; // Import JSON
|
|
1
|
+
"use client"
|
|
10
2
|
|
|
11
|
-
|
|
3
|
+
import { ArrowUpRight, ChevronDown, ChevronUp } from "lucide-react"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import Image from "next/image"
|
|
6
|
+
import Search from "@/components/SearchBox"
|
|
7
|
+
import Anchor from "@/components/anchor"
|
|
8
|
+
import { Separator } from "@/components/ui/separator"
|
|
9
|
+
import docuConfig from "@/docu.json"
|
|
10
|
+
import GitHubButton from "@/components/Github"
|
|
11
|
+
import { Button } from "@/components/ui/button"
|
|
12
|
+
import { useState, useCallback } from "react"
|
|
13
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
14
|
+
import { ModeToggle } from "@/components/ThemeToggle"
|
|
15
|
+
|
|
16
|
+
interface NavbarProps {
|
|
17
|
+
id?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Navbar({ id }: NavbarProps) {
|
|
21
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
|
22
|
+
|
|
23
|
+
const toggleMenu = useCallback(() => {
|
|
24
|
+
setIsMenuOpen((prev) => !prev)
|
|
25
|
+
}, [])
|
|
12
26
|
|
|
13
27
|
return (
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<div className="flex items-center gap-
|
|
17
|
-
<SheetLeftbar />
|
|
28
|
+
<div className="sticky top-0 z-50 w-full">
|
|
29
|
+
<nav id={id} className="bg-background h-16 w-full border-b">
|
|
30
|
+
<div className="mx-auto flex h-full w-[95vw] items-center justify-between sm:container md:gap-2">
|
|
18
31
|
<div className="flex items-center gap-6">
|
|
19
|
-
<div className="
|
|
32
|
+
<div className="flex">
|
|
20
33
|
<Logo />
|
|
21
34
|
</div>
|
|
22
35
|
</div>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
|
|
36
|
+
<div className="flex items-center gap-0 max-md:flex-row-reverse md:gap-2">
|
|
37
|
+
<div className="text-muted-foreground hidden items-center gap-4 text-sm font-medium md:flex">
|
|
26
38
|
<NavMenu />
|
|
27
39
|
</div>
|
|
28
|
-
<
|
|
40
|
+
<Button
|
|
41
|
+
variant="ghost"
|
|
42
|
+
size="sm"
|
|
43
|
+
onClick={toggleMenu}
|
|
44
|
+
aria-label={isMenuOpen ? "Close navigation menu" : "Open navigation menu"}
|
|
45
|
+
aria-expanded={isMenuOpen}
|
|
46
|
+
className="flex items-center gap-1 px-2 text-sm font-medium md:hidden"
|
|
47
|
+
>
|
|
48
|
+
{isMenuOpen ? (
|
|
49
|
+
<ChevronUp className="text-muted-foreground h-6 w-6" />
|
|
50
|
+
) : (
|
|
51
|
+
<ChevronDown className="text-muted-foreground h-6 w-6" />
|
|
52
|
+
)}
|
|
53
|
+
</Button>
|
|
54
|
+
|
|
55
|
+
<Separator className="my-4 hidden h-9 md:flex" orientation="vertical" />
|
|
29
56
|
<Search />
|
|
57
|
+
<div className="hidden md:flex">
|
|
58
|
+
<GitHubButton />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
30
61
|
</div>
|
|
31
|
-
</
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
</nav>
|
|
63
|
+
|
|
64
|
+
<AnimatePresence>
|
|
65
|
+
{isMenuOpen && (
|
|
66
|
+
<motion.div
|
|
67
|
+
initial={{ opacity: 0, height: 0 }}
|
|
68
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
69
|
+
exit={{ opacity: 0, height: 0 }}
|
|
70
|
+
transition={{ duration: 0.2, ease: "easeInOut" }}
|
|
71
|
+
className="bg-background/95 w-full border-b shadow-sm backdrop-blur-sm md:hidden"
|
|
72
|
+
>
|
|
73
|
+
<div className="mx-auto w-[95vw] sm:container">
|
|
74
|
+
<ul className="flex flex-col py-2">
|
|
75
|
+
<NavMenuCollapsible onItemClick={() => setIsMenuOpen(false)} />
|
|
76
|
+
</ul>
|
|
77
|
+
<div className="flex items-center justify-between border-t px-1 py-3">
|
|
78
|
+
<GitHubButton />
|
|
79
|
+
<ModeToggle />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</motion.div>
|
|
83
|
+
)}
|
|
84
|
+
</AnimatePresence>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
34
87
|
}
|
|
35
88
|
|
|
36
89
|
export function Logo() {
|
|
37
|
-
|
|
90
|
+
const { navbar } = docuConfig
|
|
38
91
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
return (
|
|
93
|
+
<Link href="/" className="flex items-center gap-1.5">
|
|
94
|
+
<div className="relative h-8 w-8">
|
|
95
|
+
<Image
|
|
96
|
+
src={navbar.logo.src}
|
|
97
|
+
alt={navbar.logo.alt}
|
|
98
|
+
fill
|
|
99
|
+
sizes="32px"
|
|
100
|
+
className="object-contain"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
<h2 className="font-code dark:text-accent text-primary text-lg font-bold">
|
|
104
|
+
{navbar.logoText}
|
|
105
|
+
</h2>
|
|
106
|
+
</Link>
|
|
107
|
+
)
|
|
53
108
|
}
|
|
54
109
|
|
|
55
|
-
|
|
56
|
-
|
|
110
|
+
// Desktop NavMenu — horizontal list
|
|
111
|
+
export function NavMenu() {
|
|
112
|
+
const { navbar } = docuConfig
|
|
57
113
|
|
|
58
114
|
return (
|
|
59
115
|
<>
|
|
60
116
|
{navbar?.menu?.map((item) => {
|
|
61
|
-
const isExternal = item.href.startsWith("http")
|
|
62
|
-
|
|
63
|
-
const Comp = (
|
|
117
|
+
const isExternal = item.href.startsWith("http")
|
|
118
|
+
return (
|
|
64
119
|
<Anchor
|
|
65
120
|
key={`${item.title}-${item.href}`}
|
|
66
121
|
activeClassName="text-primary dark:text-accent md:font-semibold font-medium"
|
|
67
122
|
absolute
|
|
68
|
-
className="
|
|
123
|
+
className="text-foreground/80 hover:text-foreground flex items-center gap-1 transition-colors"
|
|
69
124
|
href={item.href}
|
|
70
125
|
target={isExternal ? "_blank" : undefined}
|
|
71
126
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
72
127
|
>
|
|
73
128
|
{item.title}
|
|
74
|
-
{isExternal && <ArrowUpRight className="
|
|
129
|
+
{isExternal && <ArrowUpRight className="text-foreground/80 h-4 w-4" />}
|
|
75
130
|
</Anchor>
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
)
|
|
132
|
+
})}
|
|
133
|
+
</>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Mobile Collapsible NavMenu — vertical list items
|
|
138
|
+
function NavMenuCollapsible({ onItemClick }: { onItemClick: () => void }) {
|
|
139
|
+
const { navbar } = docuConfig
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
143
|
+
{navbar?.menu?.map((item) => {
|
|
144
|
+
const isExternal = item.href.startsWith("http")
|
|
145
|
+
return (
|
|
146
|
+
<li key={item.title + item.href}>
|
|
147
|
+
<Anchor
|
|
148
|
+
activeClassName="text-primary dark:text-accent font-semibold"
|
|
149
|
+
absolute
|
|
150
|
+
className="text-foreground/80 hover:text-foreground hover:bg-muted flex w-full items-center justify-between gap-2 rounded-md px-3 py-2.5 text-sm font-medium transition-colors"
|
|
151
|
+
href={item.href}
|
|
152
|
+
target={isExternal ? "_blank" : undefined}
|
|
153
|
+
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
154
|
+
onClick={onItemClick}
|
|
155
|
+
>
|
|
156
|
+
{item.title}
|
|
157
|
+
{isExternal && <ArrowUpRight className="text-foreground/60 h-4 w-4 shrink-0" />}
|
|
158
|
+
</Anchor>
|
|
159
|
+
</li>
|
|
160
|
+
)
|
|
84
161
|
})}
|
|
85
162
|
</>
|
|
86
|
-
)
|
|
163
|
+
)
|
|
87
164
|
}
|
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
import TocObserver from "./toc-observer";
|
|
3
|
-
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
4
|
-
import { ListIcon } from "lucide-react";
|
|
5
|
-
import Sponsor from "./Sponsor";
|
|
1
|
+
"use client"
|
|
6
2
|
|
|
3
|
+
import TocObserver from "./TocObserver"
|
|
4
|
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
5
|
+
import { ListIcon } from "lucide-react"
|
|
6
|
+
import Sponsor from "./Sponsor"
|
|
7
|
+
import { useActiveSection } from "@/hooks"
|
|
8
|
+
import { TocItem } from "@/lib/toc"
|
|
7
9
|
|
|
8
|
-
export default
|
|
9
|
-
const
|
|
10
|
+
export default function Toc({ tocs }: { tocs: TocItem[] }) {
|
|
11
|
+
const { activeId, setActiveId } = useActiveSection(tocs)
|
|
10
12
|
|
|
11
13
|
return (
|
|
12
|
-
<div className="
|
|
13
|
-
<div className="
|
|
14
|
+
<div className="toc flex-3 sticky top-4 hidden h-[calc(100vh-8rem)] min-w-[238px] self-start lg:flex lg:p-8">
|
|
15
|
+
<div className="mb-auto flex h-full w-full flex-col gap-2 px-2">
|
|
14
16
|
<div className="flex items-center gap-2">
|
|
15
|
-
<ListIcon className="
|
|
16
|
-
<h3 className="font-medium
|
|
17
|
+
<ListIcon className="h-4 w-4" />
|
|
18
|
+
<h3 className="text-sm font-medium">On this page</h3>
|
|
17
19
|
</div>
|
|
18
|
-
<div className="
|
|
20
|
+
<div className="max-h-[calc(70vh-2rem)] min-h-0 shrink-0">
|
|
19
21
|
<ScrollArea className="h-full">
|
|
20
|
-
<TocObserver data={tocs} />
|
|
22
|
+
<TocObserver data={tocs} activeId={activeId} onActiveIdChange={setActiveId} />
|
|
21
23
|
</ScrollArea>
|
|
22
24
|
</div>
|
|
23
25
|
<Sponsor />
|
|
24
26
|
</div>
|
|
25
27
|
</div>
|
|
26
|
-
)
|
|
28
|
+
)
|
|
27
29
|
}
|
|
@@ -2,7 +2,7 @@ import { PropsWithChildren } from "react";
|
|
|
2
2
|
|
|
3
3
|
export function Typography({ children }: PropsWithChildren) {
|
|
4
4
|
return (
|
|
5
|
-
<div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background prose-headings:scroll-
|
|
5
|
+
<div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background max-lg:prose-headings:scroll-mt-54 prose-headings:scroll-mt-4 w-[85vw] sm:w-full sm:mx-auto prose-code:text-sm prose-code:leading-6 dark:prose-code:text-white prose-code:text-stone-800 prose-code:p-1 prose-code:rounded-md prose-code:border pt-2 min-w-full! prose-img:rounded-md prose-img:border prose-code:before:content-none prose-code:after:content-none prose-code:px-1.5 prose-code:overflow-x-auto max-w-[500px]! prose-img:my-3 prose-h2:my-4 prose-h2:mt-8">
|
|
6
6
|
{children}
|
|
7
7
|
</div>
|
|
8
8
|
);
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState, useMemo } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
|
|
6
|
+
interface Icon {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
z: number;
|
|
10
|
+
scale: number;
|
|
11
|
+
opacity: number;
|
|
12
|
+
id: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IconCloudProps {
|
|
16
|
+
icons?: React.ReactNode[];
|
|
17
|
+
images?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function easeOutCubic(t: number): number {
|
|
21
|
+
return 1 - Math.pow(1 - t, 3);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function IconCloud({ icons, images }: IconCloudProps) {
|
|
25
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
26
|
+
// const [iconPositions, setIconPositions] = useState<Icon[]>([]);
|
|
27
|
+
const iconPositions = useMemo<Icon[]>(() => {
|
|
28
|
+
const items = icons || images || [];
|
|
29
|
+
const newIcons: Icon[] = [];
|
|
30
|
+
const numIcons = items.length || 20;
|
|
31
|
+
|
|
32
|
+
// Fibonacci sphere parameters
|
|
33
|
+
const offset = 2 / numIcons;
|
|
34
|
+
const increment = Math.PI * (3 - Math.sqrt(5));
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < numIcons; i++) {
|
|
37
|
+
const y = i * offset - 1 + offset / 2;
|
|
38
|
+
const r = Math.sqrt(1 - y * y);
|
|
39
|
+
const phi = i * increment;
|
|
40
|
+
|
|
41
|
+
const x = Math.cos(phi) * r;
|
|
42
|
+
const z = Math.sin(phi) * r;
|
|
43
|
+
|
|
44
|
+
newIcons.push({
|
|
45
|
+
x: x * 100,
|
|
46
|
+
y: y * 100,
|
|
47
|
+
z: z * 100,
|
|
48
|
+
scale: 1,
|
|
49
|
+
opacity: 1,
|
|
50
|
+
id: i,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return newIcons;
|
|
54
|
+
}, [icons, images]);
|
|
55
|
+
const [rotation] = useState({ x: 0, y: 0 });
|
|
56
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
57
|
+
const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
|
|
58
|
+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
59
|
+
const [targetRotation, setTargetRotation] = useState<{
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
startX: number;
|
|
63
|
+
startY: number;
|
|
64
|
+
distance: number;
|
|
65
|
+
startTime: number;
|
|
66
|
+
duration: number;
|
|
67
|
+
} | null>(null);
|
|
68
|
+
const animationFrameRef = useRef<number>(undefined);
|
|
69
|
+
const rotationRef = useRef(rotation);
|
|
70
|
+
const iconCanvasesRef = useRef<HTMLCanvasElement[]>([]);
|
|
71
|
+
const imagesLoadedRef = useRef<boolean[]>([]);
|
|
72
|
+
|
|
73
|
+
// Create icon canvases once when icons/images change
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!icons && !images) return;
|
|
76
|
+
|
|
77
|
+
const items = icons || images || [];
|
|
78
|
+
imagesLoadedRef.current = new Array(items.length).fill(false);
|
|
79
|
+
|
|
80
|
+
const newIconCanvases = items.map((item, index) => {
|
|
81
|
+
const offscreen = document.createElement("canvas");
|
|
82
|
+
offscreen.width = 40;
|
|
83
|
+
offscreen.height = 40;
|
|
84
|
+
const offCtx = offscreen.getContext("2d");
|
|
85
|
+
|
|
86
|
+
if (offCtx) {
|
|
87
|
+
if (images) {
|
|
88
|
+
// Handle image URLs directly
|
|
89
|
+
const img = new Image();
|
|
90
|
+
img.crossOrigin = "anonymous";
|
|
91
|
+
img.src = items[index] as string;
|
|
92
|
+
img.onload = () => {
|
|
93
|
+
offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
|
|
94
|
+
|
|
95
|
+
// Create circular clipping path
|
|
96
|
+
offCtx.beginPath();
|
|
97
|
+
offCtx.arc(20, 20, 20, 0, Math.PI * 2);
|
|
98
|
+
offCtx.closePath();
|
|
99
|
+
offCtx.clip();
|
|
100
|
+
|
|
101
|
+
// Draw the image
|
|
102
|
+
offCtx.drawImage(img, 0, 0, 40, 40);
|
|
103
|
+
|
|
104
|
+
imagesLoadedRef.current[index] = true;
|
|
105
|
+
};
|
|
106
|
+
} else {
|
|
107
|
+
// Handle SVG icons
|
|
108
|
+
offCtx.scale(0.4, 0.4);
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
const svgString = renderToString(item as React.ReactElement<any>);
|
|
111
|
+
const img = new Image();
|
|
112
|
+
img.src = "data:image/svg+xml;base64," + btoa(svgString);
|
|
113
|
+
img.onload = () => {
|
|
114
|
+
offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
|
|
115
|
+
offCtx.drawImage(img, 0, 0);
|
|
116
|
+
imagesLoadedRef.current[index] = true;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return offscreen;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
iconCanvasesRef.current = newIconCanvases;
|
|
124
|
+
}, [icons, images]);
|
|
125
|
+
|
|
126
|
+
// Generate initial icon positions on a sphere
|
|
127
|
+
// useEffect(() => {
|
|
128
|
+
// const items = icons || images || [];
|
|
129
|
+
// const newIcons: Icon[] = [];
|
|
130
|
+
// const numIcons = items.length || 20;
|
|
131
|
+
|
|
132
|
+
// // Fibonacci sphere parameters
|
|
133
|
+
// const offset = 2 / numIcons;
|
|
134
|
+
// const increment = Math.PI * (3 - Math.sqrt(5));
|
|
135
|
+
|
|
136
|
+
// for (let i = 0; i < numIcons; i++) {
|
|
137
|
+
// const y = i * offset - 1 + offset / 2;
|
|
138
|
+
// const r = Math.sqrt(1 - y * y);
|
|
139
|
+
// const phi = i * increment;
|
|
140
|
+
|
|
141
|
+
// const x = Math.cos(phi) * r;
|
|
142
|
+
// const z = Math.sin(phi) * r;
|
|
143
|
+
|
|
144
|
+
// newIcons.push({
|
|
145
|
+
// x: x * 100,
|
|
146
|
+
// y: y * 100,
|
|
147
|
+
// z: z * 100,
|
|
148
|
+
// scale: 1,
|
|
149
|
+
// opacity: 1,
|
|
150
|
+
// id: i,
|
|
151
|
+
// });
|
|
152
|
+
// }
|
|
153
|
+
// setIconPositions(newIcons);
|
|
154
|
+
// }, [icons, images]);
|
|
155
|
+
|
|
156
|
+
// Handle mouse events
|
|
157
|
+
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
158
|
+
const rect = canvasRef.current?.getBoundingClientRect();
|
|
159
|
+
if (!rect || !canvasRef.current) return;
|
|
160
|
+
|
|
161
|
+
const x = e.clientX - rect.left;
|
|
162
|
+
const y = e.clientY - rect.top;
|
|
163
|
+
|
|
164
|
+
const ctx = canvasRef.current.getContext("2d");
|
|
165
|
+
if (!ctx) return;
|
|
166
|
+
|
|
167
|
+
iconPositions.forEach((icon) => {
|
|
168
|
+
const cosX = Math.cos(rotationRef.current.x);
|
|
169
|
+
const sinX = Math.sin(rotationRef.current.x);
|
|
170
|
+
const cosY = Math.cos(rotationRef.current.y);
|
|
171
|
+
const sinY = Math.sin(rotationRef.current.y);
|
|
172
|
+
|
|
173
|
+
const rotatedX = icon.x * cosY - icon.z * sinY;
|
|
174
|
+
const rotatedZ = icon.x * sinY + icon.z * cosY;
|
|
175
|
+
const rotatedY = icon.y * cosX + rotatedZ * sinX;
|
|
176
|
+
|
|
177
|
+
const screenX = canvasRef.current!.width / 2 + rotatedX;
|
|
178
|
+
const screenY = canvasRef.current!.height / 2 + rotatedY;
|
|
179
|
+
|
|
180
|
+
const scale = (rotatedZ + 200) / 300;
|
|
181
|
+
const radius = 20 * scale;
|
|
182
|
+
const dx = x - screenX;
|
|
183
|
+
const dy = y - screenY;
|
|
184
|
+
|
|
185
|
+
if (dx * dx + dy * dy < radius * radius) {
|
|
186
|
+
const targetX = -Math.atan2(
|
|
187
|
+
icon.y,
|
|
188
|
+
Math.sqrt(icon.x * icon.x + icon.z * icon.z),
|
|
189
|
+
);
|
|
190
|
+
const targetY = Math.atan2(icon.x, icon.z);
|
|
191
|
+
|
|
192
|
+
const currentX = rotationRef.current.x;
|
|
193
|
+
const currentY = rotationRef.current.y;
|
|
194
|
+
const distance = Math.sqrt(
|
|
195
|
+
Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const duration = Math.min(2000, Math.max(800, distance * 1000));
|
|
199
|
+
|
|
200
|
+
setTargetRotation({
|
|
201
|
+
x: targetX,
|
|
202
|
+
y: targetY,
|
|
203
|
+
startX: currentX,
|
|
204
|
+
startY: currentY,
|
|
205
|
+
distance,
|
|
206
|
+
startTime: performance.now(),
|
|
207
|
+
duration,
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
setIsDragging(true);
|
|
214
|
+
setLastMousePos({ x: e.clientX, y: e.clientY });
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
218
|
+
const rect = canvasRef.current?.getBoundingClientRect();
|
|
219
|
+
if (rect) {
|
|
220
|
+
const x = e.clientX - rect.left;
|
|
221
|
+
const y = e.clientY - rect.top;
|
|
222
|
+
setMousePos({ x, y });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (isDragging) {
|
|
226
|
+
const deltaX = e.clientX - lastMousePos.x;
|
|
227
|
+
const deltaY = e.clientY - lastMousePos.y;
|
|
228
|
+
|
|
229
|
+
rotationRef.current = {
|
|
230
|
+
x: rotationRef.current.x + deltaY * 0.002,
|
|
231
|
+
y: rotationRef.current.y + deltaX * 0.002,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
setLastMousePos({ x: e.clientX, y: e.clientY });
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleMouseUp = () => {
|
|
239
|
+
setIsDragging(false);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Animation and rendering
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
const canvas = canvasRef.current;
|
|
245
|
+
const ctx = canvas?.getContext("2d");
|
|
246
|
+
if (!canvas || !ctx) return;
|
|
247
|
+
|
|
248
|
+
const animate = () => {
|
|
249
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
250
|
+
|
|
251
|
+
const centerX = canvas.width / 2;
|
|
252
|
+
const centerY = canvas.height / 2;
|
|
253
|
+
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
|
|
254
|
+
const dx = mousePos.x - centerX;
|
|
255
|
+
const dy = mousePos.y - centerY;
|
|
256
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
257
|
+
const speed = 0.003 + (distance / maxDistance) * 0.01;
|
|
258
|
+
|
|
259
|
+
if (targetRotation) {
|
|
260
|
+
const elapsed = performance.now() - targetRotation.startTime;
|
|
261
|
+
const progress = Math.min(1, elapsed / targetRotation.duration);
|
|
262
|
+
const easedProgress = easeOutCubic(progress);
|
|
263
|
+
|
|
264
|
+
rotationRef.current = {
|
|
265
|
+
x:
|
|
266
|
+
targetRotation.startX +
|
|
267
|
+
(targetRotation.x - targetRotation.startX) * easedProgress,
|
|
268
|
+
y:
|
|
269
|
+
targetRotation.startY +
|
|
270
|
+
(targetRotation.y - targetRotation.startY) * easedProgress,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (progress >= 1) {
|
|
274
|
+
setTargetRotation(null);
|
|
275
|
+
}
|
|
276
|
+
} else if (!isDragging) {
|
|
277
|
+
rotationRef.current = {
|
|
278
|
+
x: rotationRef.current.x + (dy / canvas.height) * speed,
|
|
279
|
+
y: rotationRef.current.y + (dx / canvas.width) * speed,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
iconPositions.forEach((icon, index) => {
|
|
284
|
+
const cosX = Math.cos(rotationRef.current.x);
|
|
285
|
+
const sinX = Math.sin(rotationRef.current.x);
|
|
286
|
+
const cosY = Math.cos(rotationRef.current.y);
|
|
287
|
+
const sinY = Math.sin(rotationRef.current.y);
|
|
288
|
+
|
|
289
|
+
const rotatedX = icon.x * cosY - icon.z * sinY;
|
|
290
|
+
const rotatedZ = icon.x * sinY + icon.z * cosY;
|
|
291
|
+
const rotatedY = icon.y * cosX + rotatedZ * sinX;
|
|
292
|
+
|
|
293
|
+
const scale = (rotatedZ + 200) / 300;
|
|
294
|
+
const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200));
|
|
295
|
+
|
|
296
|
+
ctx.save();
|
|
297
|
+
ctx.translate(
|
|
298
|
+
canvas.width / 2 + rotatedX,
|
|
299
|
+
canvas.height / 2 + rotatedY,
|
|
300
|
+
);
|
|
301
|
+
ctx.scale(scale, scale);
|
|
302
|
+
ctx.globalAlpha = opacity;
|
|
303
|
+
|
|
304
|
+
if (icons || images) {
|
|
305
|
+
// Only try to render icons/images if they exist
|
|
306
|
+
if (
|
|
307
|
+
iconCanvasesRef.current[index] &&
|
|
308
|
+
imagesLoadedRef.current[index]
|
|
309
|
+
) {
|
|
310
|
+
ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40);
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
// Show numbered circles if no icons/images are provided
|
|
314
|
+
ctx.beginPath();
|
|
315
|
+
ctx.arc(0, 0, 20, 0, Math.PI * 2);
|
|
316
|
+
ctx.fillStyle = "#4444ff";
|
|
317
|
+
ctx.fill();
|
|
318
|
+
ctx.fillStyle = "white";
|
|
319
|
+
ctx.textAlign = "center";
|
|
320
|
+
ctx.textBaseline = "middle";
|
|
321
|
+
ctx.font = "16px Arial";
|
|
322
|
+
ctx.fillText(`${icon.id + 1}`, 0, 0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
ctx.restore();
|
|
326
|
+
});
|
|
327
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
animate();
|
|
331
|
+
|
|
332
|
+
return () => {
|
|
333
|
+
if (animationFrameRef.current) {
|
|
334
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}, [icons, images, iconPositions, isDragging, mousePos, targetRotation]);
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<canvas
|
|
341
|
+
ref={canvasRef}
|
|
342
|
+
width={400}
|
|
343
|
+
height={400}
|
|
344
|
+
onMouseDown={handleMouseDown}
|
|
345
|
+
onMouseMove={handleMouseMove}
|
|
346
|
+
onMouseUp={handleMouseUp}
|
|
347
|
+
onMouseLeave={handleMouseUp}
|
|
348
|
+
className="rounded-full"
|
|
349
|
+
aria-label="Interactive 3D Icon Cloud"
|
|
350
|
+
role="img"
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
@@ -33,9 +33,9 @@ const ScrollBar = React.forwardRef<
|
|
|
33
33
|
className={cn(
|
|
34
34
|
"flex touch-none select-none transition-colors",
|
|
35
35
|
orientation === "vertical" &&
|
|
36
|
-
|
|
36
|
+
"h-full w-2.5 border-l border-l-transparent p-px",
|
|
37
37
|
orientation === "horizontal" &&
|
|
38
|
-
|
|
38
|
+
"h-2.5 flex-col border-t border-t-transparent p-px",
|
|
39
39
|
className
|
|
40
40
|
)}
|
|
41
41
|
{...props}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
|
5
5
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
-
import {
|
|
6
|
+
import { PanelRightClose } from "lucide-react";
|
|
7
7
|
|
|
8
8
|
import { cn } from "@/lib/utils";
|
|
9
9
|
|
|
@@ -51,7 +51,7 @@ const sheetVariants = cva(
|
|
|
51
51
|
|
|
52
52
|
interface SheetContentProps
|
|
53
53
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
54
|
-
|
|
54
|
+
VariantProps<typeof sheetVariants> { }
|
|
55
55
|
|
|
56
56
|
const SheetContent = React.forwardRef<
|
|
57
57
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
@@ -65,8 +65,8 @@ const SheetContent = React.forwardRef<
|
|
|
65
65
|
{...props}
|
|
66
66
|
>
|
|
67
67
|
{children}
|
|
68
|
-
<SheetPrimitive.Close className="absolute right-4
|
|
69
|
-
<
|
|
68
|
+
<SheetPrimitive.Close className="absolute top-7 right-4 z-50 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
|
69
|
+
<PanelRightClose className="w-6 h-6 shrink-0 text-muted-foreground" />
|
|
70
70
|
<span className="sr-only">Close</span>
|
|
71
71
|
</SheetPrimitive.Close>
|
|
72
72
|
</SheetPrimitive.Content>
|