specra 0.1.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/LICENSE.MD +21 -0
- package/README.md +157 -0
- package/dist/app/api/mdx-watch/route.d.mts +12 -0
- package/dist/app/api/mdx-watch/route.d.ts +12 -0
- package/dist/app/api/mdx-watch/route.js +98 -0
- package/dist/app/api/mdx-watch/route.js.map +1 -0
- package/dist/app/api/mdx-watch/route.mjs +71 -0
- package/dist/app/api/mdx-watch/route.mjs.map +1 -0
- package/dist/app/docs-page.d.mts +32 -0
- package/dist/app/docs-page.d.ts +32 -0
- package/dist/app/docs-page.js +4072 -0
- package/dist/app/docs-page.js.map +1 -0
- package/dist/app/docs-page.mjs +14 -0
- package/dist/app/docs-page.mjs.map +1 -0
- package/dist/app/layout.css +297 -0
- package/dist/app/layout.css.map +1 -0
- package/dist/app/layout.d.mts +19 -0
- package/dist/app/layout.d.ts +19 -0
- package/dist/app/layout.js +112 -0
- package/dist/app/layout.js.map +1 -0
- package/dist/app/layout.mjs +13 -0
- package/dist/app/layout.mjs.map +1 -0
- package/dist/chunk-DR4EPLMT.mjs +1013 -0
- package/dist/chunk-DR4EPLMT.mjs.map +1 -0
- package/dist/chunk-INL2EC72.mjs +170 -0
- package/dist/chunk-INL2EC72.mjs.map +1 -0
- package/dist/chunk-IZFGEAD6.mjs +61 -0
- package/dist/chunk-IZFGEAD6.mjs.map +1 -0
- package/dist/chunk-KTRWWAGL.mjs +50 -0
- package/dist/chunk-KTRWWAGL.mjs.map +1 -0
- package/dist/chunk-MZJHJ6BV.mjs +21 -0
- package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
- package/dist/chunk-NXRIAL7T.mjs +3119 -0
- package/dist/chunk-NXRIAL7T.mjs.map +1 -0
- package/dist/components/index.d.mts +822 -0
- package/dist/components/index.d.ts +822 -0
- package/dist/components/index.js +3738 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +3627 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/index.css +297 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +4648 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +347 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/index.d.mts +798 -0
- package/dist/lib/index.d.ts +798 -0
- package/dist/lib/index.js +1301 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/index.mjs +89 -0
- package/dist/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/app/api/mdx-watch/route.ts +86 -0
- package/src/app/docs-page.tsx +212 -0
- package/src/app/layout.tsx +74 -0
- package/src/components/docs/accordion.tsx +53 -0
- package/src/components/docs/api/api-endpoint.tsx +59 -0
- package/src/components/docs/api/api-params.tsx +43 -0
- package/src/components/docs/api/api-playground.tsx +233 -0
- package/src/components/docs/api/api-reference.tsx +291 -0
- package/src/components/docs/api/api-response.tsx +48 -0
- package/src/components/docs/api/index.ts +5 -0
- package/src/components/docs/badge.tsx +22 -0
- package/src/components/docs/breadcrumb.tsx +51 -0
- package/src/components/docs/callout.tsx +109 -0
- package/src/components/docs/card.tsx +84 -0
- package/src/components/docs/category-index.tsx +112 -0
- package/src/components/docs/code-block.tsx +129 -0
- package/src/components/docs/columns.tsx +45 -0
- package/src/components/docs/componentTextProps.ts +85 -0
- package/src/components/docs/dev-mode-badge.tsx +35 -0
- package/src/components/docs/doc-layout-wrapper.tsx +54 -0
- package/src/components/docs/doc-layout.tsx +111 -0
- package/src/components/docs/doc-loading.tsx +15 -0
- package/src/components/docs/doc-metadata.tsx +55 -0
- package/src/components/docs/doc-navigation.tsx +62 -0
- package/src/components/docs/doc-tags.tsx +25 -0
- package/src/components/docs/draft-badge.tsx +10 -0
- package/src/components/docs/footer.tsx +47 -0
- package/src/components/docs/frame.tsx +22 -0
- package/src/components/docs/header.tsx +122 -0
- package/src/components/docs/hot-reload-indicator.tsx +77 -0
- package/src/components/docs/icon.tsx +70 -0
- package/src/components/docs/image-card.tsx +95 -0
- package/src/components/docs/image.tsx +73 -0
- package/src/components/docs/index.ts +48 -0
- package/src/components/docs/math.tsx +46 -0
- package/src/components/docs/mdx-components.tsx +166 -0
- package/src/components/docs/mdx-hot-reload.tsx +37 -0
- package/src/components/docs/mermaid.tsx +77 -0
- package/src/components/docs/mobile-doc-layout.tsx +115 -0
- package/src/components/docs/not-found-content.tsx +55 -0
- package/src/components/docs/search-highlight.tsx +127 -0
- package/src/components/docs/search-modal.tsx +223 -0
- package/src/components/docs/sidebar-skeleton.tsx +39 -0
- package/src/components/docs/sidebar.tsx +323 -0
- package/src/components/docs/site-banner.tsx +92 -0
- package/src/components/docs/steps.tsx +29 -0
- package/src/components/docs/tab-context.tsx +28 -0
- package/src/components/docs/tab-groups.tsx +50 -0
- package/src/components/docs/table-of-contents.tsx +104 -0
- package/src/components/docs/tabs.tsx +63 -0
- package/src/components/docs/theme-toggle.tsx +39 -0
- package/src/components/docs/tooltip.tsx +37 -0
- package/src/components/docs/version-switcher.tsx +52 -0
- package/src/components/docs/video.tsx +80 -0
- package/src/components/global/index.ts +3 -0
- package/src/components/global/version-not-found.tsx +26 -0
- package/src/components/index.ts +8 -0
- package/src/components/theme-provider.tsx +11 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/index.ts +41 -0
- package/src/lib/api-parser.types.ts +78 -0
- package/src/lib/api.types.ts +202 -0
- package/src/lib/category.ts +71 -0
- package/src/lib/config.server.ts +170 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/config.types.ts +295 -0
- package/src/lib/dev-utils.ts +75 -0
- package/src/lib/index.ts +27 -0
- package/src/lib/mdx-cache.ts +200 -0
- package/src/lib/mdx.ts +402 -0
- package/src/lib/parsers/base-parser.ts +16 -0
- package/src/lib/parsers/index.ts +69 -0
- package/src/lib/parsers/openapi-parser.ts +251 -0
- package/src/lib/parsers/postman-parser.ts +301 -0
- package/src/lib/parsers/specra-parser.ts +24 -0
- package/src/lib/redirects.ts +40 -0
- package/src/lib/remark-code-meta.ts +23 -0
- package/src/lib/sidebar-utils.ts +188 -0
- package/src/lib/toc.ts +24 -0
- package/src/lib/utils.ts +36 -0
- package/src/specra.config.json +124 -0
- package/src/styles/globals.css +427 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
interface DocNavigationProps {
|
|
5
|
+
previousDoc?: {
|
|
6
|
+
title: string
|
|
7
|
+
slug: string
|
|
8
|
+
}
|
|
9
|
+
nextDoc?: {
|
|
10
|
+
title: string
|
|
11
|
+
slug: string
|
|
12
|
+
}
|
|
13
|
+
version: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DocNavigation({ previousDoc, nextDoc, version }: DocNavigationProps) {
|
|
17
|
+
if (!previousDoc && !nextDoc) return null
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="mt-12 pt-8 border-t border-border grid grid-cols-2 gap-4">
|
|
21
|
+
{previousDoc ? (
|
|
22
|
+
<Link
|
|
23
|
+
href={`/docs/${version}/${previousDoc.slug}`}
|
|
24
|
+
className="group flex flex-col gap-2 p-4 rounded-xl border border-border hover:border-primary/50 hover:bg-muted/50 transition-all"
|
|
25
|
+
style={{
|
|
26
|
+
textDecoration: "none !important"
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
30
|
+
<ChevronLeft className="h-4 w-4" />
|
|
31
|
+
<span>Previous</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="text-base font-medium text-foreground group-hover:text-primary transition-colors">
|
|
34
|
+
{previousDoc.title}
|
|
35
|
+
</div>
|
|
36
|
+
</Link>
|
|
37
|
+
) : (
|
|
38
|
+
<div />
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{nextDoc ? (
|
|
42
|
+
<Link
|
|
43
|
+
href={`/docs/${version}/${nextDoc.slug}`}
|
|
44
|
+
className="group flex flex-col gap-2 p-4 rounded-xl border border-border hover:border-primary/50 hover:bg-muted/50 transition-all text-right"
|
|
45
|
+
style={{
|
|
46
|
+
textDecoration: "none !important"
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<div className="flex items-center justify-end gap-2 text-sm text-muted-foreground">
|
|
50
|
+
<span>Next</span>
|
|
51
|
+
<ChevronRight className="h-4 w-4" />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="text-base font-medium text-foreground group-hover:text-primary transition-colors">
|
|
54
|
+
{nextDoc.title}
|
|
55
|
+
</div>
|
|
56
|
+
</Link>
|
|
57
|
+
) : (
|
|
58
|
+
<div />
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Tag } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
interface DocTagsProps {
|
|
4
|
+
tags: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function DocTags({ tags }: DocTagsProps) {
|
|
8
|
+
if (!tags || tags.length === 0) {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex flex-wrap items-center gap-2 mt-6 pt-6 border-t border-border">
|
|
14
|
+
<Tag className="h-4 w-4 text-muted-foreground" />
|
|
15
|
+
{tags.map((tag) => (
|
|
16
|
+
<span
|
|
17
|
+
key={tag}
|
|
18
|
+
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20"
|
|
19
|
+
>
|
|
20
|
+
{tag}
|
|
21
|
+
</span>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FileWarning } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
export function DraftBadge() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-yellow-500/10 border border-yellow-500/20 text-yellow-600 dark:text-yellow-400 text-sm font-medium mb-4">
|
|
6
|
+
<FileWarning className="h-4 w-4" />
|
|
7
|
+
<span>Draft - Not visible in production</span>
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { getConfig, SpecraConfig } from "@/lib/config"
|
|
3
|
+
|
|
4
|
+
export function Footer({ config }: { config: SpecraConfig }) {
|
|
5
|
+
// Server component - can use getConfig directly
|
|
6
|
+
// const config = getConfig()
|
|
7
|
+
|
|
8
|
+
if (!config.footer) {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<footer className="bg-muted/30 dark:bg-muted/10 rounded-2xl mt-24">
|
|
14
|
+
<div className="px-6 py-12">
|
|
15
|
+
{config.footer.links && config.footer.links.length > 0 && (
|
|
16
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
|
|
17
|
+
{config.footer.links.map((column, idx) => (
|
|
18
|
+
<div key={idx}>
|
|
19
|
+
<h3 className="font-semibold text-foreground mb-4">{column.title}</h3>
|
|
20
|
+
<ul className="space-y-2">
|
|
21
|
+
{column.items.map((item, itemIdx) => (
|
|
22
|
+
<li key={itemIdx}>
|
|
23
|
+
<Link
|
|
24
|
+
href={item.href}
|
|
25
|
+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
26
|
+
>
|
|
27
|
+
{item.label}
|
|
28
|
+
</Link>
|
|
29
|
+
</li>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
{config.footer.copyright && (
|
|
38
|
+
<div className="pt-8">
|
|
39
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
40
|
+
{config.footer.copyright}
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
</footer>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface FrameProps {
|
|
2
|
+
src: string
|
|
3
|
+
title?: string
|
|
4
|
+
height?: number | string
|
|
5
|
+
width?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Frame({ src, title = "Embedded content", height = 500, width = "100%" }: FrameProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="my-6 rounded-xl border border-border overflow-hidden bg-muted/30">
|
|
11
|
+
<iframe
|
|
12
|
+
src={src}
|
|
13
|
+
title={title}
|
|
14
|
+
width={width}
|
|
15
|
+
height={height}
|
|
16
|
+
className="w-full"
|
|
17
|
+
loading="lazy"
|
|
18
|
+
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { Search, Menu, Github, Twitter, MessageCircle } from "lucide-react"
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
import { VersionSwitcher } from "./version-switcher"
|
|
7
|
+
import { ThemeToggle } from "./theme-toggle"
|
|
8
|
+
import { SearchModal } from "./search-modal"
|
|
9
|
+
import { useState, useEffect } from "react"
|
|
10
|
+
import type { SpecraConfig } from "@/lib/config"
|
|
11
|
+
import { getAssetPath } from "@/lib/utils"
|
|
12
|
+
|
|
13
|
+
interface HeaderProps {
|
|
14
|
+
currentVersion: string
|
|
15
|
+
versions: string[]
|
|
16
|
+
onMenuClick?: () => void
|
|
17
|
+
config: SpecraConfig
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Header({ currentVersion, versions, onMenuClick, config }: HeaderProps) {
|
|
21
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
22
|
+
|
|
23
|
+
// Keyboard shortcut for search (Cmd+K or Ctrl+K)
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
26
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
27
|
+
e.preventDefault()
|
|
28
|
+
setSearchOpen(true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
window.addEventListener("keydown", handleKeyDown)
|
|
33
|
+
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
|
38
|
+
<div className="container flex h-16 items-center justify-between px-6 mx-auto">
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<button
|
|
41
|
+
onClick={onMenuClick}
|
|
42
|
+
className="lg:hidden hover:bg-muted p-2 rounded-md transition-colors"
|
|
43
|
+
aria-label="Toggle menu"
|
|
44
|
+
>
|
|
45
|
+
<Menu className="h-5 w-5" />
|
|
46
|
+
</button>
|
|
47
|
+
<Link href="/" className="flex items-center gap-2">
|
|
48
|
+
{config.site.logo ? (
|
|
49
|
+
<img src={getAssetPath(config.site.logo)} alt={config.site.title} className="h-8 w-auto" />
|
|
50
|
+
) : (
|
|
51
|
+
<div className="h-8 w-8 rounded-xl bg-primary flex items-center justify-center">
|
|
52
|
+
<span className="text-primary-foreground font-bold text-lg">
|
|
53
|
+
{config.site.title.charAt(0).toUpperCase()}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
<span className="font-semibold text-lg text-foreground">Specra</span>
|
|
58
|
+
</Link>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
{config.search?.enabled && (
|
|
63
|
+
<button
|
|
64
|
+
onClick={() => setSearchOpen(true)}
|
|
65
|
+
className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground bg-muted rounded-md transition-colors"
|
|
66
|
+
>
|
|
67
|
+
<Search className="h-4 w-4" />
|
|
68
|
+
<span className="hidden sm:inline">{config.search.placeholder || "Search"}</span>
|
|
69
|
+
<kbd className="hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border border-border bg-background px-1.5 font-mono text-xs font-medium">
|
|
70
|
+
⌘K
|
|
71
|
+
</kbd>
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{config.features?.versioning && (
|
|
76
|
+
<VersionSwitcher currentVersion={currentVersion} versions={versions} />
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Social Links */}
|
|
80
|
+
{config.social?.github && (
|
|
81
|
+
<a
|
|
82
|
+
href={config.social.github}
|
|
83
|
+
target="_blank"
|
|
84
|
+
rel="noopener noreferrer"
|
|
85
|
+
className="hidden md:flex items-center justify-center h-9 w-9 rounded-md hover:bg-muted transition-colors"
|
|
86
|
+
aria-label="GitHub"
|
|
87
|
+
>
|
|
88
|
+
<Github className="h-4 w-4" />
|
|
89
|
+
</a>
|
|
90
|
+
)}
|
|
91
|
+
{config.social?.twitter && (
|
|
92
|
+
<a
|
|
93
|
+
href={config.social.twitter}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
className="hidden md:flex items-center justify-center h-9 w-9 rounded-md hover:bg-muted transition-colors"
|
|
97
|
+
aria-label="Twitter"
|
|
98
|
+
>
|
|
99
|
+
<Twitter className="h-4 w-4" />
|
|
100
|
+
</a>
|
|
101
|
+
)}
|
|
102
|
+
{config.social?.discord && (
|
|
103
|
+
<a
|
|
104
|
+
href={config.social.discord}
|
|
105
|
+
target="_blank"
|
|
106
|
+
rel="noopener noreferrer"
|
|
107
|
+
className="hidden md:flex items-center justify-center h-9 w-9 rounded-md hover:bg-muted transition-colors"
|
|
108
|
+
aria-label="Discord"
|
|
109
|
+
>
|
|
110
|
+
<MessageCircle className="h-4 w-4" />
|
|
111
|
+
</a>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<ThemeToggle />
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Search Modal */}
|
|
119
|
+
<SearchModal isOpen={searchOpen} onClose={() => setSearchOpen(false)} config={config} />
|
|
120
|
+
</header>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
import { RefreshCw } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
export function HotReloadIndicator() {
|
|
8
|
+
const [isReloading, setIsReloading] = useState(false)
|
|
9
|
+
const [lastReload, setLastReload] = useState<Date | null>(null)
|
|
10
|
+
const pathname = usePathname()
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (process.env.NODE_ENV !== "development") return
|
|
14
|
+
|
|
15
|
+
// Track when content updates
|
|
16
|
+
setIsReloading(true)
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
setIsReloading(false)
|
|
19
|
+
setLastReload(new Date())
|
|
20
|
+
|
|
21
|
+
// Auto-hide after 3 seconds
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
setLastReload(null)
|
|
24
|
+
}, 3000)
|
|
25
|
+
}, 500)
|
|
26
|
+
|
|
27
|
+
return () => clearTimeout(timer)
|
|
28
|
+
}, [pathname])
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (process.env.NODE_ENV !== "development") return
|
|
32
|
+
|
|
33
|
+
// Listen for Next.js Fast Refresh
|
|
34
|
+
const handleBeforeRefresh = () => {
|
|
35
|
+
setIsReloading(true)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleAfterRefresh = () => {
|
|
39
|
+
setIsReloading(false)
|
|
40
|
+
setLastReload(new Date())
|
|
41
|
+
setTimeout(() => setLastReload(null), 3000)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// @ts-ignore - Next.js internal API
|
|
45
|
+
if (typeof window !== 'undefined' && window.__NEXT_DATA__) {
|
|
46
|
+
window.addEventListener('beforeunload', handleBeforeRefresh)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener('beforeunload', handleBeforeRefresh)
|
|
51
|
+
}
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
if (process.env.NODE_ENV !== "development") return null
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{/* Reloading indicator */}
|
|
59
|
+
{isReloading && (
|
|
60
|
+
<div className="fixed bottom-4 right-4 z-50 flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-xl shadow-lg animate-in slide-in-from-bottom-2">
|
|
61
|
+
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
62
|
+
<span className="text-sm font-medium">Reloading...</span>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{/* Success indicator */}
|
|
67
|
+
{lastReload && !isReloading && (
|
|
68
|
+
<div className="fixed bottom-4 right-4 z-50 flex items-center gap-2 px-4 py-2 bg-green-500 text-white rounded-xl shadow-lg animate-in slide-in-from-bottom-2">
|
|
69
|
+
<RefreshCw className="h-4 w-4" />
|
|
70
|
+
<span className="text-sm font-medium">
|
|
71
|
+
Updated at {lastReload.toLocaleTimeString()}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as LucideIcons from "lucide-react"
|
|
4
|
+
|
|
5
|
+
interface IconProps {
|
|
6
|
+
icon: string | React.ReactNode
|
|
7
|
+
iconType?: "regular" | "solid" | "light" | "thin" | "sharp-solid" | "duotone" | "brands"
|
|
8
|
+
color?: string
|
|
9
|
+
size?: number
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Icon({ icon, iconType = "regular", color, size = 20, className = "" }: IconProps) {
|
|
14
|
+
// If icon is a React node (custom SVG), render it directly
|
|
15
|
+
if (typeof icon !== "string") {
|
|
16
|
+
return <span className={`inline-flex items-center ${className}`} style={{ color }}>{icon}</span>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if it's a URL (external or local file)
|
|
20
|
+
if (icon.startsWith("http") || icon.startsWith("/")) {
|
|
21
|
+
return (
|
|
22
|
+
<img
|
|
23
|
+
src={icon}
|
|
24
|
+
alt=""
|
|
25
|
+
width={size}
|
|
26
|
+
height={size}
|
|
27
|
+
className={`inline-block ${className}`}
|
|
28
|
+
style={{ color }}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if it's a Font Awesome icon (starts with fa-)
|
|
34
|
+
if (icon.startsWith("fa-")) {
|
|
35
|
+
const faClass = `fa-${iconType} ${icon}`
|
|
36
|
+
return (
|
|
37
|
+
<i
|
|
38
|
+
className={`${faClass} ${className}`}
|
|
39
|
+
style={{ fontSize: size, color }}
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Try to find Lucide icon
|
|
46
|
+
const iconName = icon
|
|
47
|
+
.split("-")
|
|
48
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
49
|
+
.join("")
|
|
50
|
+
|
|
51
|
+
const LucideIcon = (LucideIcons as any)[iconName]
|
|
52
|
+
|
|
53
|
+
if (LucideIcon) {
|
|
54
|
+
return (
|
|
55
|
+
<LucideIcon
|
|
56
|
+
size={size}
|
|
57
|
+
className={`inline-block ${className}`}
|
|
58
|
+
style={{ color }}
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback: render the icon name
|
|
65
|
+
return (
|
|
66
|
+
<span className={`inline-flex items-center font-mono text-xs ${className}`} style={{ color }}>
|
|
67
|
+
[{icon}]
|
|
68
|
+
</span>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import NextImage from "next/image"
|
|
2
|
+
import Link from "next/link"
|
|
3
|
+
|
|
4
|
+
interface ImageCardProps {
|
|
5
|
+
src: string
|
|
6
|
+
alt: string
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
href?: string
|
|
10
|
+
external?: boolean
|
|
11
|
+
aspectRatio?: "square" | "video" | "portrait"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ImageCard({
|
|
15
|
+
src,
|
|
16
|
+
alt,
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
href,
|
|
20
|
+
external = false,
|
|
21
|
+
aspectRatio = "video",
|
|
22
|
+
}: ImageCardProps) {
|
|
23
|
+
const aspectRatios = {
|
|
24
|
+
square: "aspect-square",
|
|
25
|
+
video: "aspect-video",
|
|
26
|
+
portrait: "aspect-[3/4]",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = (
|
|
30
|
+
<div className="flex flex-col gap-0 p-0">
|
|
31
|
+
<div className={`w-full ${aspectRatios[aspectRatio]} overflow-hidden ${(title || description) ? 'rounded-t-xl' : 'rounded-xl'} bg-muted relative`}>
|
|
32
|
+
<NextImage
|
|
33
|
+
src={src}
|
|
34
|
+
alt={alt}
|
|
35
|
+
fill
|
|
36
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
37
|
+
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
{(title || description) && (
|
|
41
|
+
<div className="p-3 flex flex-col gap-1">
|
|
42
|
+
{title && (
|
|
43
|
+
<h3 className={`font-semibold text-foreground mb-0 no-underline ${href ? 'group-hover:text-primary transition-colors' : ''}`}>
|
|
44
|
+
{title}
|
|
45
|
+
</h3>
|
|
46
|
+
)}
|
|
47
|
+
{description && (
|
|
48
|
+
<p className="text-sm text-muted-foreground line-clamp-2 no-underline mb-0">
|
|
49
|
+
{description}
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (href) {
|
|
58
|
+
const Component = external ? "a" : Link
|
|
59
|
+
return (
|
|
60
|
+
<Component
|
|
61
|
+
href={href}
|
|
62
|
+
className="image-card-link group block rounded-xl border border-border hover:border-primary/50 hover:shadow-lg transition-all overflow-hidden p-0"
|
|
63
|
+
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
64
|
+
>
|
|
65
|
+
{content}
|
|
66
|
+
</Component>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="block rounded-xl border border-border overflow-hidden bg-card p-0">
|
|
72
|
+
{content}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ImageCardGridProps {
|
|
78
|
+
children: React.ReactNode
|
|
79
|
+
cols?: 1 | 2 | 3 | 4
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function ImageCardGrid({ children, cols = 3 }: ImageCardGridProps) {
|
|
83
|
+
const gridCols = {
|
|
84
|
+
1: "grid-cols-1",
|
|
85
|
+
2: "grid-cols-1 md:grid-cols-2",
|
|
86
|
+
3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
|
87
|
+
4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={`grid ${gridCols[cols]} gap-4 my-6`}>
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import NextImage from "next/image"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { ZoomIn, X } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
interface ImageProps {
|
|
8
|
+
src: string
|
|
9
|
+
alt: string
|
|
10
|
+
caption?: string
|
|
11
|
+
width?: number
|
|
12
|
+
height?: number
|
|
13
|
+
zoom?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Image({ src, alt, caption, width, height, zoom = true }: ImageProps) {
|
|
17
|
+
const [isZoomed, setIsZoomed] = useState(false)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<figure className="my-6">
|
|
22
|
+
<div className="relative group rounded-xl border border-border overflow-hidden bg-muted/30">
|
|
23
|
+
<NextImage
|
|
24
|
+
src={src}
|
|
25
|
+
alt={alt}
|
|
26
|
+
width={width || 1200}
|
|
27
|
+
height={height || 675}
|
|
28
|
+
className="w-full h-auto"
|
|
29
|
+
/>
|
|
30
|
+
{zoom && (
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => setIsZoomed(true)}
|
|
33
|
+
className="absolute top-3 right-3 p-2 rounded-md bg-background/80 backdrop-blur-sm border border-border opacity-0 group-hover:opacity-100 transition-opacity hover:bg-background"
|
|
34
|
+
aria-label="Zoom image"
|
|
35
|
+
>
|
|
36
|
+
<ZoomIn className="h-4 w-4 text-foreground" />
|
|
37
|
+
</button>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
{caption && (
|
|
41
|
+
<figcaption className="mt-2 text-center text-sm text-muted-foreground italic">
|
|
42
|
+
{caption}
|
|
43
|
+
</figcaption>
|
|
44
|
+
)}
|
|
45
|
+
</figure>
|
|
46
|
+
|
|
47
|
+
{/* Zoom Modal */}
|
|
48
|
+
{isZoomed && (
|
|
49
|
+
<div
|
|
50
|
+
className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex items-center justify-center p-4"
|
|
51
|
+
onClick={() => setIsZoomed(false)}
|
|
52
|
+
>
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => setIsZoomed(false)}
|
|
55
|
+
className="absolute top-4 right-4 p-2 rounded-md bg-muted hover:bg-muted/80 transition-colors"
|
|
56
|
+
aria-label="Close"
|
|
57
|
+
>
|
|
58
|
+
<X className="h-5 w-5 text-foreground" />
|
|
59
|
+
</button>
|
|
60
|
+
<div className="max-w-7xl max-h-[90vh] overflow-auto">
|
|
61
|
+
<NextImage
|
|
62
|
+
src={src}
|
|
63
|
+
alt={alt}
|
|
64
|
+
width={width || 1920}
|
|
65
|
+
height={height || 1080}
|
|
66
|
+
className="w-full h-auto"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Core documentation components
|
|
2
|
+
export * from "./accordion"
|
|
3
|
+
export { Badge as DocBadge } from "./badge"
|
|
4
|
+
export * from "./breadcrumb"
|
|
5
|
+
export * from "./callout"
|
|
6
|
+
export * from "./card"
|
|
7
|
+
export * from "./category-index"
|
|
8
|
+
export * from "./code-block"
|
|
9
|
+
export * from "./columns"
|
|
10
|
+
export * from "./componentTextProps"
|
|
11
|
+
export * from "./dev-mode-badge"
|
|
12
|
+
export * from "./doc-layout"
|
|
13
|
+
export * from "./doc-layout-wrapper"
|
|
14
|
+
export * from "./doc-loading"
|
|
15
|
+
export * from "./doc-metadata"
|
|
16
|
+
export * from "./doc-navigation"
|
|
17
|
+
export * from "./doc-tags"
|
|
18
|
+
export * from "./draft-badge"
|
|
19
|
+
export * from "./footer"
|
|
20
|
+
export * from "./frame"
|
|
21
|
+
export * from "./header"
|
|
22
|
+
export * from "./hot-reload-indicator"
|
|
23
|
+
export * from "./icon"
|
|
24
|
+
export * from "./image-card"
|
|
25
|
+
export * from "./image"
|
|
26
|
+
export * from "./math"
|
|
27
|
+
export * from "./mdx-components"
|
|
28
|
+
export * from "./mdx-hot-reload"
|
|
29
|
+
export * from "./mermaid"
|
|
30
|
+
export * from "./mobile-doc-layout"
|
|
31
|
+
export * from "./not-found-content"
|
|
32
|
+
export * from "./search-highlight"
|
|
33
|
+
export * from "./search-modal"
|
|
34
|
+
export * from "./sidebar-skeleton"
|
|
35
|
+
export * from "./sidebar"
|
|
36
|
+
export * from "./site-banner"
|
|
37
|
+
export * from "./steps"
|
|
38
|
+
export * from "./tab-context"
|
|
39
|
+
export * from "./tab-groups"
|
|
40
|
+
export * from "./table-of-contents"
|
|
41
|
+
export * from "./tabs"
|
|
42
|
+
export * from "./theme-toggle"
|
|
43
|
+
export * from "./tooltip"
|
|
44
|
+
export * from "./version-switcher"
|
|
45
|
+
export * from "./video"
|
|
46
|
+
|
|
47
|
+
// API documentation components
|
|
48
|
+
export * from "./api"
|