@docubook/create 1.16.1 → 2.0.0-beta.2
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 +22 -11
- package/src/dist/app/layout.tsx +3 -0
- package/src/dist/app/page.tsx +6 -6
- package/src/dist/components/SearchModal.tsx +41 -37
- package/src/dist/components/context-popover.tsx +3 -2
- package/src/dist/components/contexts/theme-provider.tsx +1 -2
- package/src/dist/components/leftbar.tsx +1 -1
- package/src/dist/components/markdown/AccordionMdx.tsx +6 -6
- package/src/dist/components/markdown/CardGroupMdx.tsx +10 -2
- package/src/dist/components/markdown/CopyMdx.tsx +1 -1
- package/src/dist/components/markdown/FileTreeMdx.tsx +2 -2
- package/src/dist/components/markdown/ImageMdx.tsx +122 -18
- package/src/dist/components/markdown/NoteMdx.tsx +46 -29
- package/src/dist/components/markdown/PreMdx.tsx +1 -1
- package/src/dist/components/mob-toc.tsx +1 -1
- package/src/dist/components/scroll-to-top.tsx +1 -0
- package/src/dist/components/sublink.tsx +2 -1
- package/src/dist/components/theme-toggle.tsx +38 -37
- package/src/dist/components/ui/tabs.tsx +1 -1
- package/src/dist/eslint.config.mjs +35 -0
- package/src/dist/hooks/useScrollPosition.ts +6 -5
- package/src/dist/package.json +44 -39
- package/src/dist/postcss.config.js +1 -1
- package/src/dist/styles/globals.css +230 -111
- package/src/dist/tailwind.config.ts +107 -105
- package/src/dist/tsconfig.json +21 -6
- package/src/dist/components/ui/icon-cloud.tsx +0 -324
package/package.json
CHANGED
|
@@ -12,13 +12,19 @@ import MobToc from "@/components/mob-toc";
|
|
|
12
12
|
const { meta } = docuConfig;
|
|
13
13
|
|
|
14
14
|
type PageProps = {
|
|
15
|
-
params: {
|
|
15
|
+
params: Promise<{
|
|
16
16
|
slug: string[];
|
|
17
|
-
}
|
|
17
|
+
}>;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// Function to generate metadata dynamically
|
|
21
|
-
export async function generateMetadata(
|
|
21
|
+
export async function generateMetadata(props: PageProps) {
|
|
22
|
+
const params = await props.params;
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
slug = []
|
|
26
|
+
} = params;
|
|
27
|
+
|
|
22
28
|
const pathName = slug.join("/");
|
|
23
29
|
const res = await getDocsForSlug(pathName);
|
|
24
30
|
|
|
@@ -62,7 +68,13 @@ export async function generateMetadata({ params: { slug = [] } }: PageProps) {
|
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
export default async function DocsPage(
|
|
71
|
+
export default async function DocsPage(props: PageProps) {
|
|
72
|
+
const params = await props.params;
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
slug = []
|
|
76
|
+
} = params;
|
|
77
|
+
|
|
66
78
|
const pathName = slug.join("/");
|
|
67
79
|
const res = await getDocsForSlug(pathName);
|
|
68
80
|
|
|
@@ -85,17 +97,16 @@ export default async function DocsPage({ params: { slug = [] } }: PageProps) {
|
|
|
85
97
|
<p className="-mt-4 text-muted-foreground text-[16.5px]">{description}</p>
|
|
86
98
|
<div>{res.content}</div>
|
|
87
99
|
<div
|
|
88
|
-
className={`my-8 flex items-center border-b-2 border-dashed border-x-muted-foreground ${
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
>
|
|
100
|
+
className={`my-8 flex items-center border-b-2 border-dashed border-x-muted-foreground ${docuConfig.repository?.editLink ? "justify-between" : "justify-end"
|
|
101
|
+
}`}
|
|
102
|
+
>
|
|
92
103
|
{docuConfig.repository?.editLink && <EditThisPage filePath={filePath} />}
|
|
93
104
|
{date && (
|
|
94
|
-
|
|
105
|
+
<p className="text-[13px] text-muted-foreground">
|
|
95
106
|
Published on {formatDate2(date)}
|
|
96
|
-
|
|
107
|
+
</p>
|
|
97
108
|
)}
|
|
98
|
-
|
|
109
|
+
</div>
|
|
99
110
|
<Pagination pathname={pathName} />
|
|
100
111
|
</Typography>
|
|
101
112
|
</div>
|
package/src/dist/app/layout.tsx
CHANGED
|
@@ -6,6 +6,9 @@ import { GeistMono } from "geist/font/mono";
|
|
|
6
6
|
import { Footer } from "@/components/footer";
|
|
7
7
|
import docuConfig from "@/docu.json";
|
|
8
8
|
import { Toaster } from "@/components/ui/sonner";
|
|
9
|
+
import "@docsearch/css";
|
|
10
|
+
import "@/styles/algolia.css";
|
|
11
|
+
import "@/styles/syntax.css";
|
|
9
12
|
import "@/styles/globals.css";
|
|
10
13
|
|
|
11
14
|
const { meta } = docuConfig;
|
package/src/dist/app/page.tsx
CHANGED
|
@@ -25,21 +25,21 @@ export default function Home() {
|
|
|
25
25
|
)}
|
|
26
26
|
>
|
|
27
27
|
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
|
|
28
|
-
<span>🚀
|
|
28
|
+
<span>🚀 Release v2.0.0-beta.2</span>
|
|
29
29
|
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
|
30
30
|
</AnimatedShinyText>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
</Link>
|
|
34
|
-
|
|
34
|
+
<div className="w-full max-w-[800px] pb-8">
|
|
35
35
|
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">DocuBook Starter Templates</h1>
|
|
36
36
|
<p className="mb-8 sm:text-xl text-muted-foreground">
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Get started by editing app/page.tsx . Save and see your changes instantly.{' '}
|
|
38
|
+
<Link className="text-primary underline" href="https://www.docubook.pro/docs/getting-started/introduction" target="_blank">
|
|
39
39
|
Read Documentations
|
|
40
|
-
|
|
40
|
+
</Link>
|
|
41
41
|
</p>
|
|
42
|
-
|
|
42
|
+
</div>
|
|
43
43
|
<div className="flex flex-row items-center gap-6 mb-10">
|
|
44
44
|
<Link
|
|
45
45
|
href={`/docs${page_routes[0].href}`}
|
|
@@ -50,6 +50,7 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
50
50
|
|
|
51
51
|
useEffect(() => {
|
|
52
52
|
if (!isOpen) {
|
|
53
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
53
54
|
setSearchedInput("");
|
|
54
55
|
}
|
|
55
56
|
}, [isOpen]);
|
|
@@ -71,9 +72,9 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
71
72
|
return advanceSearch(trimmedInput) as unknown as SearchResult[];
|
|
72
73
|
}, [searchedInput]);
|
|
73
74
|
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
|
|
76
|
-
}, [filteredResults]);
|
|
75
|
+
// useEffect(() => {
|
|
76
|
+
// setSelectedIndex(0);
|
|
77
|
+
// }, [filteredResults]);
|
|
77
78
|
|
|
78
79
|
useEffect(() => {
|
|
79
80
|
const handleNavigation = (event: KeyboardEvent) => {
|
|
@@ -114,10 +115,13 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
114
115
|
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
|
|
115
116
|
<DialogDescription className="sr-only">Search through the documentation</DialogDescription>
|
|
116
117
|
</DialogHeader>
|
|
117
|
-
|
|
118
|
+
|
|
118
119
|
<input
|
|
119
120
|
value={searchedInput}
|
|
120
|
-
onChange={(e) =>
|
|
121
|
+
onChange={(e) => {
|
|
122
|
+
setSearchedInput(e.target.value);
|
|
123
|
+
setSelectedIndex(0);
|
|
124
|
+
}}
|
|
121
125
|
placeholder="Type something to search..."
|
|
122
126
|
autoFocus
|
|
123
127
|
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
|
|
@@ -138,38 +142,38 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
138
142
|
const isActive = index === selectedIndex;
|
|
139
143
|
|
|
140
144
|
return (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
<DialogClose key={item.href} asChild>
|
|
146
|
+
<Anchor
|
|
147
|
+
ref={(el) => {
|
|
148
|
+
itemRefs.current[index] = el as HTMLDivElement | null;
|
|
149
|
+
}}
|
|
150
|
+
className={cn(
|
|
151
|
+
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
|
|
152
|
+
isActive && "bg-primary/20 dark:bg-primary/30",
|
|
153
|
+
paddingClass
|
|
154
|
+
)}
|
|
155
|
+
href={`/docs${item.href}`}
|
|
156
|
+
tabIndex={-1}
|
|
157
|
+
>
|
|
158
|
+
<div
|
|
146
159
|
className={cn(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
paddingClass
|
|
160
|
+
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
|
|
161
|
+
level > 1 && "border-l pl-4"
|
|
150
162
|
)}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
className={cn(
|
|
156
|
-
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
|
|
157
|
-
level > 1 && "border-l pl-4"
|
|
158
|
-
)}
|
|
159
|
-
>
|
|
160
|
-
<div className="flex items-center">
|
|
161
|
-
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
|
|
162
|
-
<span>{item.title}</span>
|
|
163
|
-
</div>
|
|
164
|
-
{isActive && (
|
|
165
|
-
<div className="hidden md:flex items-center text-xs text-muted-foreground">
|
|
166
|
-
<span>Return</span>
|
|
167
|
-
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
163
|
+
>
|
|
164
|
+
<div className="flex items-center">
|
|
165
|
+
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
|
|
166
|
+
<span>{item.title}</span>
|
|
170
167
|
</div>
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
{isActive && (
|
|
169
|
+
<div className="hidden md:flex items-center text-xs text-muted-foreground">
|
|
170
|
+
<span>Return</span>
|
|
171
|
+
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</Anchor>
|
|
176
|
+
</DialogClose>
|
|
173
177
|
);
|
|
174
178
|
})}
|
|
175
179
|
</div>
|
|
@@ -177,14 +181,14 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
177
181
|
<DialogFooter className="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
|
|
178
182
|
<div className="flex items-center gap-2">
|
|
179
183
|
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
|
180
|
-
<ArrowUpIcon className="w-3 h-3"/>
|
|
184
|
+
<ArrowUpIcon className="w-3 h-3" />
|
|
181
185
|
</span>
|
|
182
186
|
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
|
183
|
-
<ArrowDownIcon className="w-3 h-3"/>
|
|
187
|
+
<ArrowDownIcon className="w-3 h-3" />
|
|
184
188
|
</span>
|
|
185
189
|
<p className="text-muted-foreground">to navigate</p>
|
|
186
190
|
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
|
187
|
-
<CornerDownLeftIcon className="w-3 h-3"/>
|
|
191
|
+
<CornerDownLeftIcon className="w-3 h-3" />
|
|
188
192
|
</span>
|
|
189
193
|
<p className="text-muted-foreground">to select</p>
|
|
190
194
|
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1">
|
|
@@ -45,6 +45,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
|
|
45
45
|
|
|
46
46
|
useEffect(() => {
|
|
47
47
|
if (pathname.startsWith("/docs")) {
|
|
48
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
48
49
|
setActiveRoute(getActiveContextRoute(pathname));
|
|
49
50
|
} else {
|
|
50
51
|
setActiveRoute(undefined);
|
|
@@ -61,7 +62,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
|
|
61
62
|
<Button
|
|
62
63
|
variant="ghost"
|
|
63
64
|
className={cn(
|
|
64
|
-
"w-full max-w-[240px] flex items-center justify-between font-semibold text-foreground px-0 pt-8",
|
|
65
|
+
"w-full max-w-[240px] cursor-pointer flex items-center justify-between font-semibold text-foreground px-0 pt-8",
|
|
65
66
|
"hover:bg-transparent hover:text-foreground",
|
|
66
67
|
className
|
|
67
68
|
)}
|
|
@@ -95,7 +96,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
|
|
95
96
|
key={route.href}
|
|
96
97
|
onClick={() => router.push(contextPath)}
|
|
97
98
|
className={cn(
|
|
98
|
-
"relative flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm",
|
|
99
|
+
"relative flex w-full items-center gap-2 cursor-pointer rounded px-2 py-1.5 text-sm",
|
|
99
100
|
"text-left outline-none transition-colors",
|
|
100
101
|
isActive
|
|
101
102
|
? "bg-primary/20 text-primary dark:bg-accent/20 dark:text-accent"
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import * as React from "react";
|
|
4
3
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
5
|
-
import { type ThemeProviderProps } from "next-themes
|
|
4
|
+
import { type ThemeProviderProps } from "next-themes";
|
|
6
5
|
|
|
7
6
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
8
7
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
|
@@ -29,7 +29,7 @@ export function ToggleButton({
|
|
|
29
29
|
<Button
|
|
30
30
|
size="icon"
|
|
31
31
|
variant="outline"
|
|
32
|
-
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
|
|
32
|
+
className="cursor-pointer hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
|
|
33
33
|
onClick={onToggle}
|
|
34
34
|
>
|
|
35
35
|
{collapsed ? (
|
|
@@ -4,7 +4,7 @@ import { ReactNode, useState, useContext } from 'react';
|
|
|
4
4
|
import { ChevronRight } from 'lucide-react';
|
|
5
5
|
import * as Icons from "lucide-react";
|
|
6
6
|
import { cn } from '@/lib/utils';
|
|
7
|
-
import { AccordionGroupContext } from '@/components/contexts/AccordionContext';
|
|
7
|
+
import { AccordionGroupContext } from '@/components/contexts/AccordionContext';
|
|
8
8
|
|
|
9
9
|
type AccordionProps = {
|
|
10
10
|
title: string;
|
|
@@ -27,7 +27,7 @@ const Accordion: React.FC<AccordionProps> = ({
|
|
|
27
27
|
// The main wrapper div for the accordion.
|
|
28
28
|
// All styling logic for the accordion container is handled here.
|
|
29
29
|
return (
|
|
30
|
-
<div
|
|
30
|
+
<div
|
|
31
31
|
className={cn(
|
|
32
32
|
// Style for STANDALONE: full card with border & shadow
|
|
33
33
|
!isInGroup && "border rounded-lg shadow-sm",
|
|
@@ -38,16 +38,16 @@ const Accordion: React.FC<AccordionProps> = ({
|
|
|
38
38
|
<button
|
|
39
39
|
type="button"
|
|
40
40
|
onClick={() => setIsOpen(!isOpen)}
|
|
41
|
-
className="flex items-center
|
|
41
|
+
className="flex items-center gap-2 w-full px-4 h-12 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70 cursor-pointer"
|
|
42
42
|
>
|
|
43
43
|
<ChevronRight
|
|
44
44
|
className={cn(
|
|
45
|
-
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
45
|
+
"w-4 h-4 text-muted-foreground transition-transform duration-200 flex-shrink-0",
|
|
46
46
|
isOpen && "rotate-90"
|
|
47
47
|
)}
|
|
48
48
|
/>
|
|
49
|
-
{Icon && <Icon className="text-foreground w-4 h-4"/>
|
|
50
|
-
<h3 className="font-medium text-base text-foreground m-0">{title}</h3>
|
|
49
|
+
{Icon && <Icon className="text-foreground w-4 h-4 flex-shrink-0" />}
|
|
50
|
+
<h3 className="font-medium text-base text-foreground !m-0 leading-none">{title}</h3>
|
|
51
51
|
</button>
|
|
52
52
|
|
|
53
53
|
{isOpen && (
|
|
@@ -8,13 +8,21 @@ interface CardGroupProps {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
|
|
11
|
-
const cardsArray = React.Children.toArray(children);
|
|
11
|
+
const cardsArray = React.Children.toArray(children);
|
|
12
|
+
|
|
13
|
+
// Static grid column classes for Tailwind v4 compatibility
|
|
14
|
+
const gridColsClass = {
|
|
15
|
+
1: "grid-cols-1",
|
|
16
|
+
2: "grid-cols-1 sm:grid-cols-2",
|
|
17
|
+
3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
18
|
+
4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
|
|
19
|
+
}[cols] || "grid-cols-1 sm:grid-cols-2";
|
|
12
20
|
|
|
13
21
|
return (
|
|
14
22
|
<div
|
|
15
23
|
className={clsx(
|
|
16
24
|
"grid gap-4 text-foreground",
|
|
17
|
-
|
|
25
|
+
gridColsClass,
|
|
18
26
|
className
|
|
19
27
|
)}
|
|
20
28
|
>
|
|
@@ -100,8 +100,8 @@ export const Files = ({ children }: { children: ReactNode }) => {
|
|
|
100
100
|
return (
|
|
101
101
|
<div
|
|
102
102
|
className="
|
|
103
|
-
rounded-xl border border-muted/
|
|
104
|
-
bg-card/
|
|
103
|
+
rounded-xl border border-muted/20
|
|
104
|
+
bg-card/20 backdrop-blur-sm
|
|
105
105
|
shadow-sm overflow-hidden
|
|
106
106
|
transition-all duration-200
|
|
107
107
|
hover:shadow-md hover:border-muted/60
|
|
@@ -1,25 +1,129 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ComponentProps, useState, useEffect } from "react";
|
|
2
4
|
import NextImage from "next/image";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
7
|
+
import { X, ZoomIn } from "lucide-react";
|
|
3
8
|
|
|
4
9
|
type Height = ComponentProps<typeof NextImage>["height"];
|
|
5
10
|
type Width = ComponentProps<typeof NextImage>["width"];
|
|
6
11
|
|
|
12
|
+
type ImageProps = Omit<ComponentProps<"img">, "src"> & {
|
|
13
|
+
src?: ComponentProps<typeof NextImage>["src"];
|
|
14
|
+
};
|
|
15
|
+
|
|
7
16
|
export default function Image({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
src,
|
|
18
|
+
alt = "alt",
|
|
19
|
+
width = 800,
|
|
20
|
+
height = 350,
|
|
21
|
+
...props
|
|
22
|
+
}: ImageProps) {
|
|
23
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
24
|
+
|
|
25
|
+
// Lock scroll when open
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (isOpen) {
|
|
28
|
+
document.body.style.overflow = "hidden";
|
|
29
|
+
// Check for Escape key
|
|
30
|
+
const handleEsc = (e: KeyboardEvent) => {
|
|
31
|
+
if (e.key === "Escape") setIsOpen(false);
|
|
32
|
+
};
|
|
33
|
+
window.addEventListener("keydown", handleEsc);
|
|
34
|
+
return () => {
|
|
35
|
+
document.body.style.overflow = "auto";
|
|
36
|
+
window.removeEventListener("keydown", handleEsc);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}, [isOpen]);
|
|
40
|
+
|
|
41
|
+
if (!src) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
className="relative group cursor-zoom-in my-6 w-full flex justify-center rounded-lg"
|
|
48
|
+
onClick={() => setIsOpen(true)}
|
|
49
|
+
aria-label="Zoom image"
|
|
50
|
+
>
|
|
51
|
+
<span className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors z-10 flex items-center justify-center opacity-0 group-hover:opacity-100 rounded-lg">
|
|
52
|
+
<ZoomIn className="w-8 h-8 text-white drop-shadow-md" />
|
|
53
|
+
</span>
|
|
54
|
+
<NextImage
|
|
55
|
+
src={src}
|
|
56
|
+
alt={alt}
|
|
57
|
+
width={width as Width}
|
|
58
|
+
height={height as Height}
|
|
59
|
+
quality={85}
|
|
60
|
+
className="w-full h-auto rounded-lg transition-transform duration-300 group-hover:scale-[1.01]"
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
<AnimatePresence>
|
|
66
|
+
{isOpen && (
|
|
67
|
+
<Portal>
|
|
68
|
+
<motion.div
|
|
69
|
+
initial={{ opacity: 0 }}
|
|
70
|
+
animate={{ opacity: 1 }}
|
|
71
|
+
exit={{ opacity: 0 }}
|
|
72
|
+
className="fixed inset-0 z-[99999] flex items-center justify-center bg-black/90 backdrop-blur-md p-4 md:p-10 cursor-zoom-out"
|
|
73
|
+
onClick={() => setIsOpen(false)}
|
|
74
|
+
>
|
|
75
|
+
{/* Close Button */}
|
|
76
|
+
<button
|
|
77
|
+
className="absolute top-4 right-4 z-50 p-2 text-white/70 hover:text-white bg-black/20 hover:bg-white/10 rounded-full transition-colors"
|
|
78
|
+
onClick={(e) => {
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
setIsOpen(false);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<X className="w-6 h-6" />
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
{/* Image Container */}
|
|
87
|
+
<motion.div
|
|
88
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
89
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
90
|
+
exit={{ scale: 0.9, opacity: 0 }}
|
|
91
|
+
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
|
92
|
+
className="relative max-w-7xl w-full h-full flex items-center justify-center"
|
|
93
|
+
onClick={(e) => e.stopPropagation()}
|
|
94
|
+
>
|
|
95
|
+
<div className="relative w-full h-full flex items-center justify-center" onClick={() => setIsOpen(false)}>
|
|
96
|
+
<NextImage
|
|
97
|
+
src={src}
|
|
98
|
+
alt={alt}
|
|
99
|
+
width={1920}
|
|
100
|
+
height={1080}
|
|
101
|
+
className="object-contain max-h-[90vh] w-auto h-auto rounded-md shadow-2xl"
|
|
102
|
+
quality={95}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</motion.div>
|
|
106
|
+
|
|
107
|
+
{/* Caption */}
|
|
108
|
+
{alt && alt !== "alt" && (
|
|
109
|
+
<motion.div
|
|
110
|
+
initial={{ y: 20, opacity: 0 }}
|
|
111
|
+
animate={{ y: 0, opacity: 1 }}
|
|
112
|
+
className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-black/60 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-md border border-white/10"
|
|
113
|
+
>
|
|
114
|
+
{alt}
|
|
115
|
+
</motion.div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
</motion.div>
|
|
119
|
+
</Portal>
|
|
120
|
+
)}
|
|
121
|
+
</AnimatePresence>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
25
124
|
}
|
|
125
|
+
|
|
126
|
+
const Portal = ({ children }: { children: React.ReactNode }) => {
|
|
127
|
+
if (typeof window === "undefined") return null;
|
|
128
|
+
return createPortal(children, document.body);
|
|
129
|
+
};
|
|
@@ -1,52 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { cn } from "@/lib/utils";
|
|
2
|
-
import
|
|
3
|
-
import { PropsWithChildren } from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
5
|
import {
|
|
5
6
|
Info,
|
|
6
7
|
AlertTriangle,
|
|
7
8
|
ShieldAlert,
|
|
8
|
-
|
|
9
|
+
CheckCircle2,
|
|
9
10
|
} from "lucide-react";
|
|
11
|
+
import React from "react";
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const noteVariants = cva(
|
|
14
|
+
"relative w-full rounded-lg border border-l-4 p-4 mb-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
variant: {
|
|
18
|
+
note: "bg-muted/30 border-border border-l-primary/50 text-foreground [&>svg]:text-primary",
|
|
19
|
+
danger: "border-destructive/20 border-l-destructive/60 bg-destructive/5 text-destructive [&>svg]:text-destructive dark:border-destructive/30",
|
|
20
|
+
warning: "border-orange-500/20 border-l-orange-500/60 bg-orange-500/5 text-orange-600 dark:text-orange-400 [&>svg]:text-orange-600 dark:[&>svg]:text-orange-400",
|
|
21
|
+
success: "border-emerald-500/20 border-l-emerald-500/60 bg-emerald-500/5 text-emerald-600 dark:text-emerald-400 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "note",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
);
|
|
15
29
|
|
|
16
30
|
const iconMap = {
|
|
17
|
-
note:
|
|
18
|
-
danger:
|
|
19
|
-
warning:
|
|
20
|
-
success:
|
|
31
|
+
note: Info,
|
|
32
|
+
danger: ShieldAlert,
|
|
33
|
+
warning: AlertTriangle,
|
|
34
|
+
success: CheckCircle2,
|
|
21
35
|
};
|
|
22
36
|
|
|
37
|
+
interface NoteProps
|
|
38
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
39
|
+
VariantProps<typeof noteVariants> {
|
|
40
|
+
title?: string;
|
|
41
|
+
type?: "note" | "danger" | "warning" | "success";
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
export default function Note({
|
|
24
|
-
|
|
45
|
+
className,
|
|
25
46
|
title = "Note",
|
|
26
47
|
type = "note",
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
27
50
|
}: NoteProps) {
|
|
28
|
-
const
|
|
29
|
-
"dark:bg-stone-950/25 bg-stone-50": type === "note",
|
|
30
|
-
"dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900":
|
|
31
|
-
type === "danger",
|
|
32
|
-
"bg-orange-50 border-orange-200 dark:border-orange-900 dark:bg-orange-900/50":
|
|
33
|
-
type === "warning",
|
|
34
|
-
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
|
|
35
|
-
type === "success",
|
|
36
|
-
});
|
|
51
|
+
const Icon = iconMap[type] || Info;
|
|
37
52
|
|
|
38
53
|
return (
|
|
39
54
|
<div
|
|
40
|
-
className={cn(
|
|
41
|
-
|
|
42
|
-
noteClassNames
|
|
43
|
-
)}
|
|
55
|
+
className={cn(noteVariants({ variant: type }), className)}
|
|
56
|
+
{...props}
|
|
44
57
|
>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
<
|
|
58
|
+
<Icon className="h-5 w-5" />
|
|
59
|
+
<div className="pl-8">
|
|
60
|
+
<h5 className="mb-1 font-medium leading-none tracking-tight">
|
|
61
|
+
{title}
|
|
62
|
+
</h5>
|
|
63
|
+
<div className="text-sm [&_p]:leading-relaxed opacity-90">
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
48
66
|
</div>
|
|
49
|
-
{children}
|
|
50
67
|
</div>
|
|
51
68
|
);
|
|
52
69
|
}
|
|
@@ -14,7 +14,7 @@ interface MobTocProps {
|
|
|
14
14
|
tocs: TocItem[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const useClickOutside = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
|
17
|
+
const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: () => void) => {
|
|
18
18
|
const handleClick = React.useCallback((event: MouseEvent) => {
|
|
19
19
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
20
20
|
callback();
|