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.
Files changed (142) hide show
  1. package/LICENSE.MD +21 -0
  2. package/README.md +157 -0
  3. package/dist/app/api/mdx-watch/route.d.mts +12 -0
  4. package/dist/app/api/mdx-watch/route.d.ts +12 -0
  5. package/dist/app/api/mdx-watch/route.js +98 -0
  6. package/dist/app/api/mdx-watch/route.js.map +1 -0
  7. package/dist/app/api/mdx-watch/route.mjs +71 -0
  8. package/dist/app/api/mdx-watch/route.mjs.map +1 -0
  9. package/dist/app/docs-page.d.mts +32 -0
  10. package/dist/app/docs-page.d.ts +32 -0
  11. package/dist/app/docs-page.js +4072 -0
  12. package/dist/app/docs-page.js.map +1 -0
  13. package/dist/app/docs-page.mjs +14 -0
  14. package/dist/app/docs-page.mjs.map +1 -0
  15. package/dist/app/layout.css +297 -0
  16. package/dist/app/layout.css.map +1 -0
  17. package/dist/app/layout.d.mts +19 -0
  18. package/dist/app/layout.d.ts +19 -0
  19. package/dist/app/layout.js +112 -0
  20. package/dist/app/layout.js.map +1 -0
  21. package/dist/app/layout.mjs +13 -0
  22. package/dist/app/layout.mjs.map +1 -0
  23. package/dist/chunk-DR4EPLMT.mjs +1013 -0
  24. package/dist/chunk-DR4EPLMT.mjs.map +1 -0
  25. package/dist/chunk-INL2EC72.mjs +170 -0
  26. package/dist/chunk-INL2EC72.mjs.map +1 -0
  27. package/dist/chunk-IZFGEAD6.mjs +61 -0
  28. package/dist/chunk-IZFGEAD6.mjs.map +1 -0
  29. package/dist/chunk-KTRWWAGL.mjs +50 -0
  30. package/dist/chunk-KTRWWAGL.mjs.map +1 -0
  31. package/dist/chunk-MZJHJ6BV.mjs +21 -0
  32. package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
  33. package/dist/chunk-NXRIAL7T.mjs +3119 -0
  34. package/dist/chunk-NXRIAL7T.mjs.map +1 -0
  35. package/dist/components/index.d.mts +822 -0
  36. package/dist/components/index.d.ts +822 -0
  37. package/dist/components/index.js +3738 -0
  38. package/dist/components/index.js.map +1 -0
  39. package/dist/components/index.mjs +3627 -0
  40. package/dist/components/index.mjs.map +1 -0
  41. package/dist/index.css +297 -0
  42. package/dist/index.css.map +1 -0
  43. package/dist/index.d.mts +545 -0
  44. package/dist/index.d.ts +545 -0
  45. package/dist/index.js +4648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +347 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/lib/index.d.mts +798 -0
  50. package/dist/lib/index.d.ts +798 -0
  51. package/dist/lib/index.js +1301 -0
  52. package/dist/lib/index.js.map +1 -0
  53. package/dist/lib/index.mjs +89 -0
  54. package/dist/lib/index.mjs.map +1 -0
  55. package/package.json +119 -0
  56. package/src/app/api/mdx-watch/route.ts +86 -0
  57. package/src/app/docs-page.tsx +212 -0
  58. package/src/app/layout.tsx +74 -0
  59. package/src/components/docs/accordion.tsx +53 -0
  60. package/src/components/docs/api/api-endpoint.tsx +59 -0
  61. package/src/components/docs/api/api-params.tsx +43 -0
  62. package/src/components/docs/api/api-playground.tsx +233 -0
  63. package/src/components/docs/api/api-reference.tsx +291 -0
  64. package/src/components/docs/api/api-response.tsx +48 -0
  65. package/src/components/docs/api/index.ts +5 -0
  66. package/src/components/docs/badge.tsx +22 -0
  67. package/src/components/docs/breadcrumb.tsx +51 -0
  68. package/src/components/docs/callout.tsx +109 -0
  69. package/src/components/docs/card.tsx +84 -0
  70. package/src/components/docs/category-index.tsx +112 -0
  71. package/src/components/docs/code-block.tsx +129 -0
  72. package/src/components/docs/columns.tsx +45 -0
  73. package/src/components/docs/componentTextProps.ts +85 -0
  74. package/src/components/docs/dev-mode-badge.tsx +35 -0
  75. package/src/components/docs/doc-layout-wrapper.tsx +54 -0
  76. package/src/components/docs/doc-layout.tsx +111 -0
  77. package/src/components/docs/doc-loading.tsx +15 -0
  78. package/src/components/docs/doc-metadata.tsx +55 -0
  79. package/src/components/docs/doc-navigation.tsx +62 -0
  80. package/src/components/docs/doc-tags.tsx +25 -0
  81. package/src/components/docs/draft-badge.tsx +10 -0
  82. package/src/components/docs/footer.tsx +47 -0
  83. package/src/components/docs/frame.tsx +22 -0
  84. package/src/components/docs/header.tsx +122 -0
  85. package/src/components/docs/hot-reload-indicator.tsx +77 -0
  86. package/src/components/docs/icon.tsx +70 -0
  87. package/src/components/docs/image-card.tsx +95 -0
  88. package/src/components/docs/image.tsx +73 -0
  89. package/src/components/docs/index.ts +48 -0
  90. package/src/components/docs/math.tsx +46 -0
  91. package/src/components/docs/mdx-components.tsx +166 -0
  92. package/src/components/docs/mdx-hot-reload.tsx +37 -0
  93. package/src/components/docs/mermaid.tsx +77 -0
  94. package/src/components/docs/mobile-doc-layout.tsx +115 -0
  95. package/src/components/docs/not-found-content.tsx +55 -0
  96. package/src/components/docs/search-highlight.tsx +127 -0
  97. package/src/components/docs/search-modal.tsx +223 -0
  98. package/src/components/docs/sidebar-skeleton.tsx +39 -0
  99. package/src/components/docs/sidebar.tsx +323 -0
  100. package/src/components/docs/site-banner.tsx +92 -0
  101. package/src/components/docs/steps.tsx +29 -0
  102. package/src/components/docs/tab-context.tsx +28 -0
  103. package/src/components/docs/tab-groups.tsx +50 -0
  104. package/src/components/docs/table-of-contents.tsx +104 -0
  105. package/src/components/docs/tabs.tsx +63 -0
  106. package/src/components/docs/theme-toggle.tsx +39 -0
  107. package/src/components/docs/tooltip.tsx +37 -0
  108. package/src/components/docs/version-switcher.tsx +52 -0
  109. package/src/components/docs/video.tsx +80 -0
  110. package/src/components/global/index.ts +3 -0
  111. package/src/components/global/version-not-found.tsx +26 -0
  112. package/src/components/index.ts +8 -0
  113. package/src/components/theme-provider.tsx +11 -0
  114. package/src/components/ui/badge.tsx +46 -0
  115. package/src/components/ui/button.tsx +60 -0
  116. package/src/components/ui/dialog.tsx +143 -0
  117. package/src/components/ui/index.ts +6 -0
  118. package/src/components/ui/input.tsx +21 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/index.ts +41 -0
  121. package/src/lib/api-parser.types.ts +78 -0
  122. package/src/lib/api.types.ts +202 -0
  123. package/src/lib/category.ts +71 -0
  124. package/src/lib/config.server.ts +170 -0
  125. package/src/lib/config.ts +20 -0
  126. package/src/lib/config.types.ts +295 -0
  127. package/src/lib/dev-utils.ts +75 -0
  128. package/src/lib/index.ts +27 -0
  129. package/src/lib/mdx-cache.ts +200 -0
  130. package/src/lib/mdx.ts +402 -0
  131. package/src/lib/parsers/base-parser.ts +16 -0
  132. package/src/lib/parsers/index.ts +69 -0
  133. package/src/lib/parsers/openapi-parser.ts +251 -0
  134. package/src/lib/parsers/postman-parser.ts +301 -0
  135. package/src/lib/parsers/specra-parser.ts +24 -0
  136. package/src/lib/redirects.ts +40 -0
  137. package/src/lib/remark-code-meta.ts +23 -0
  138. package/src/lib/sidebar-utils.ts +188 -0
  139. package/src/lib/toc.ts +24 -0
  140. package/src/lib/utils.ts +36 -0
  141. package/src/specra.config.json +124 -0
  142. 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"