@docubook/create 2.4.0 → 2.5.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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/dist/app/docs/[[...slug]]/page.tsx +55 -59
  3. package/src/dist/app/docs/layout.tsx +11 -4
  4. package/src/dist/app/layout.tsx +10 -7
  5. package/src/dist/app/page.tsx +1 -1
  6. package/src/dist/components/{context-popover.tsx → ContextPopover.tsx} +3 -2
  7. package/src/dist/components/{docs-breadcrumb.tsx → DocsBreadcrumb.tsx} +1 -1
  8. package/src/dist/components/DocsNavbar.tsx +46 -0
  9. package/src/dist/components/DocsSidebar.tsx +196 -0
  10. package/src/dist/components/Github.tsx +26 -0
  11. package/src/dist/components/{scroll-to-top.tsx → ScrollToTop.tsx} +16 -9
  12. package/src/dist/components/SearchBox.tsx +37 -0
  13. package/src/dist/components/SearchContext.tsx +47 -0
  14. package/src/dist/components/SearchModal.tsx +1 -1
  15. package/src/dist/components/SearchTrigger.tsx +5 -5
  16. package/src/dist/components/Sponsor.tsx +2 -2
  17. package/src/dist/components/{theme-toggle.tsx → ThemeToggle.tsx} +10 -10
  18. package/src/dist/components/TocObserver.tsx +197 -0
  19. package/src/dist/components/footer.tsx +16 -12
  20. package/src/dist/components/leftbar.tsx +45 -73
  21. package/src/dist/components/markdown/AccordionGroupMdx.tsx +1 -1
  22. package/src/dist/components/markdown/AccordionMdx.tsx +1 -1
  23. package/src/dist/components/navbar.tsx +130 -53
  24. package/src/dist/components/toc.tsx +16 -14
  25. package/src/dist/components/typography.tsx +1 -1
  26. package/src/dist/components/ui/icon-cloud.tsx +353 -0
  27. package/src/dist/components/ui/scroll-area.tsx +2 -2
  28. package/src/dist/components/ui/sheet.tsx +4 -4
  29. package/src/dist/components/ui/toggle.tsx +3 -3
  30. package/src/dist/hooks/useActiveSection.ts +34 -32
  31. package/src/dist/hooks/useScrollPosition.ts +16 -14
  32. package/src/dist/package.json +1 -1
  33. package/src/dist/styles/algolia.css +11 -9
  34. package/src/dist/styles/{syntax.css → override.css} +82 -39
  35. package/src/dist/tailwind.config.ts +11 -110
  36. package/src/dist/components/GithubStart.tsx +0 -44
  37. package/src/dist/components/mob-toc.tsx +0 -134
  38. package/src/dist/components/search.tsx +0 -55
  39. package/src/dist/components/toc-observer.tsx +0 -254
  40. /package/src/dist/components/{docs-menu.tsx → DocsMenu.tsx} +0 -0
  41. /package/src/dist/components/{edit-on-github.tsx → EditWithGithub.tsx} +0 -0
  42. /package/src/dist/components/{theme-provider.tsx → ThemeProvider.tsx} +0 -0
  43. /package/src/dist/{components/AccordionContext.ts → lib/accordion-context.ts} +0 -0
@@ -1,87 +1,164 @@
1
- import { ArrowUpRight } from "lucide-react";
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
- export function Navbar() {
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
- <nav className="sticky top-0 z-50 w-full h-16 border-b bg-background">
15
- <div className="sm:container mx-auto w-[95vw] h-full flex items-center justify-between md:gap-2">
16
- <div className="flex items-center gap-5">
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="hidden lg:flex">
32
+ <div className="flex">
20
33
  <Logo />
21
34
  </div>
22
35
  </div>
23
- </div>
24
- <div className="flex items-center gap-2">
25
- <div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
36
+ <div className="flex items-center md:gap-2 gap-0 max-md:flex-row-reverse">
37
+ <div className="text-muted-foreground hidden items-center gap-4 text-sm font-medium md:flex">
26
38
  <NavMenu />
27
39
  </div>
28
- <Separator className="hidden lg:flex my-4 h-9" orientation="vertical" />
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="h-6 w-6 text-muted-foreground" />
50
+ ) : (
51
+ <ChevronDown className="h-6 w-6 text-muted-foreground" />
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
- </div>
32
- </nav>
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
- const { navbar } = docuConfig; // Extract navbar from JSON
90
+ const { navbar } = docuConfig
38
91
 
39
- return (
40
- <Link href="/" className="flex items-center gap-1.5">
41
- <div className="relative w-8 h-8">
42
- <Image
43
- src={navbar.logo.src}
44
- alt={navbar.logo.alt}
45
- fill
46
- sizes="32px"
47
- className="object-contain"
48
- />
49
- </div>
50
- <h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
51
- </Link>
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
- export function NavMenu({ isSheet = false }) {
56
- const { navbar } = docuConfig; // Extract navbar from JSON
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="flex items-center gap-1 text-foreground/80 hover:text-foreground transition-colors"
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="w-4 h-4 text-foreground/80" />}
129
+ {isExternal && <ArrowUpRight className="text-foreground/80 h-4 w-4" />}
75
130
  </Anchor>
76
- );
77
- return isSheet ? (
78
- <SheetClose key={item.title + item.href} asChild>
79
- {Comp}
80
- </SheetClose>
81
- ) : (
82
- Comp
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
- import { getDocsTocs } from "@/lib/markdown";
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 async function Toc({ path }: { path: string }) {
9
- const tocs = await getDocsTocs(path);
10
+ export default function Toc({ tocs }: { tocs: TocItem[] }) {
11
+ const { activeId, setActiveId } = useActiveSection(tocs)
10
12
 
11
13
  return (
12
- <div className="lg:flex hidden toc flex-[1.5] min-w-[238px] py-5 sticky top-16 h-[calc(100vh-4rem)]">
13
- <div className="flex flex-col h-full w-full px-2 gap-2 mb-auto">
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="w-4 h-4" />
16
- <h3 className="font-medium text-sm">On this page</h3>
17
+ <ListIcon className="h-4 w-4" />
18
+ <h3 className="text-sm font-medium">On this page</h3>
17
19
  </div>
18
- <div className="flex-shrink-0 min-h-0 max-h-[calc(70vh-4rem)]">
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-m-20 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">
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-m-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
- "h-full w-2.5 border-l border-l-transparent p-[1px]",
36
+ "h-full w-2.5 border-l border-l-transparent p-px",
37
37
  orientation === "horizontal" &&
38
- "h-2.5 flex-col border-t border-t-transparent p-[1px]",
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 { X } from "lucide-react";
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
- VariantProps<typeof sheetVariants> {}
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 top-7 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
- <X className="h-4 w-4" />
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>