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,29 @@
|
|
|
1
|
+
interface StepsProps {
|
|
2
|
+
children: React.ReactNode
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface StepProps {
|
|
6
|
+
title: string
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Steps({ children }: StepsProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="my-6 ml-4 space-y-6 [counter-reset:step]">
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Step({ title, children }: StepProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="relative pl-8 pb-6 border-l-2 border-border last:border-l-0 last:pb-0 [counter-increment:step] before:content-[counter(step)] before:absolute before:left-0 before:-translate-x-1/2 before:w-8 before:h-8 before:rounded-full before:bg-primary before:text-primary-foreground before:flex before:items-center before:justify-center before:text-sm before:font-semibold before:z-10">
|
|
21
|
+
<div className="mb-2">
|
|
22
|
+
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="prose prose-sm dark:prose-invert max-w-none [&>*:last-child]:mb-0">
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, ReactNode } from "react"
|
|
4
|
+
|
|
5
|
+
interface TabContextType {
|
|
6
|
+
activeTabGroup: string
|
|
7
|
+
setActiveTabGroup: (tabId: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TabContext = createContext<TabContextType | undefined>(undefined)
|
|
11
|
+
|
|
12
|
+
export function TabProvider({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
|
|
13
|
+
const [activeTabGroup, setActiveTabGroup] = useState(defaultTab)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<TabContext.Provider value={{ activeTabGroup, setActiveTabGroup }}>
|
|
17
|
+
{children}
|
|
18
|
+
</TabContext.Provider>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useTabContext() {
|
|
23
|
+
const context = useContext(TabContext)
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error("useTabContext must be used within TabProvider")
|
|
26
|
+
}
|
|
27
|
+
return context
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Icon } from "./icon"
|
|
4
|
+
import type { TabGroup } from "@/lib/config.types"
|
|
5
|
+
|
|
6
|
+
interface TabGroupsProps {
|
|
7
|
+
tabGroups: TabGroup[]
|
|
8
|
+
activeTabId?: string
|
|
9
|
+
onTabChange?: (tabId: string) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TabGroups({ tabGroups, activeTabId, onTabChange }: TabGroupsProps) {
|
|
13
|
+
const activeTab = activeTabId || tabGroups[0]?.id || ""
|
|
14
|
+
|
|
15
|
+
const handleTabChange = (tabId: string) => {
|
|
16
|
+
onTabChange?.(tabId)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!tabGroups || tabGroups.length === 0) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="sticky top-16 z-30 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
25
|
+
<div className="container mx-auto px-6">
|
|
26
|
+
<nav className="flex gap-1 overflow-x-auto no-scrollbar" aria-label="Documentation tabs">
|
|
27
|
+
{tabGroups.map((tab) => {
|
|
28
|
+
const isActive = tab.id === activeTab
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
key={tab.id}
|
|
33
|
+
onClick={() => handleTabChange(tab.id)}
|
|
34
|
+
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-all border-b-2 ${
|
|
35
|
+
isActive
|
|
36
|
+
? "border-primary text-primary"
|
|
37
|
+
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
|
|
38
|
+
}`}
|
|
39
|
+
aria-current={isActive ? "page" : undefined}
|
|
40
|
+
>
|
|
41
|
+
{tab.icon && <Icon icon={tab.icon} size={16} className="shrink-0" />}
|
|
42
|
+
{tab.label}
|
|
43
|
+
</button>
|
|
44
|
+
)
|
|
45
|
+
})}
|
|
46
|
+
</nav>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import type { SpecraConfig } from "@/lib/config"
|
|
5
|
+
|
|
6
|
+
interface TOCItem {
|
|
7
|
+
id: string
|
|
8
|
+
title: string
|
|
9
|
+
level: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TableOfContentsProps {
|
|
13
|
+
items: TOCItem[]
|
|
14
|
+
config: SpecraConfig
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TableOfContents({ items, config }: TableOfContentsProps) {
|
|
18
|
+
const [activeId, setActiveId] = useState<string>("")
|
|
19
|
+
|
|
20
|
+
// Check if TOC should be shown
|
|
21
|
+
if (!config.navigation?.showTableOfContents) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Filter items by max depth
|
|
26
|
+
const maxDepth = config.navigation?.tocMaxDepth || 3
|
|
27
|
+
const filteredItems = items.filter(item => item.level <= maxDepth)
|
|
28
|
+
|
|
29
|
+
// Check if tab groups are configured
|
|
30
|
+
const hasTabGroups = config.navigation?.tabGroups && config.navigation.tabGroups.length > 0
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const observer = new IntersectionObserver(
|
|
34
|
+
(entries) => {
|
|
35
|
+
entries.forEach((entry) => {
|
|
36
|
+
if (entry.isIntersecting) {
|
|
37
|
+
setActiveId(entry.target.id)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
{ rootMargin: "-80px 0px -80% 0px" },
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
filteredItems.forEach((item) => {
|
|
45
|
+
const element = document.getElementById(item.id)
|
|
46
|
+
if (element) {
|
|
47
|
+
observer.observe(element)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return () => observer.disconnect()
|
|
52
|
+
}, [filteredItems])
|
|
53
|
+
|
|
54
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
const element = document.getElementById(id)
|
|
57
|
+
if (element) {
|
|
58
|
+
const offset = 100 // Offset for fixed header
|
|
59
|
+
const elementPosition = element.getBoundingClientRect().top
|
|
60
|
+
const offsetPosition = elementPosition + window.scrollY - offset
|
|
61
|
+
|
|
62
|
+
window.scrollTo({
|
|
63
|
+
top: offsetPosition,
|
|
64
|
+
behavior: "smooth",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Update URL without jumping
|
|
68
|
+
window.history.replaceState(null, "", `#${id}`)
|
|
69
|
+
|
|
70
|
+
// Manually set active ID after scroll
|
|
71
|
+
setActiveId(id)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Adjust top position based on whether tabs are present
|
|
76
|
+
const stickyTop = hasTabGroups ? "top-[7.5rem]" : "top-24"
|
|
77
|
+
const maxHeight = hasTabGroups ? "max-h-[calc(100vh-10rem)]" : "max-h-[calc(100vh-7rem)]"
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<aside className={`w-64 hidden xl:block shrink-0 sticky ${stickyTop} self-start`}>
|
|
81
|
+
{filteredItems.length > 0 && (
|
|
82
|
+
<div className={`${maxHeight} overflow-y-auto bg-muted/30 dark:bg-muted/10 rounded-2xl p-4 border border-border/50`}>
|
|
83
|
+
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">On this page</h3>
|
|
84
|
+
<nav className="space-y-1">
|
|
85
|
+
{filteredItems.map((item) => (
|
|
86
|
+
<a
|
|
87
|
+
key={item.id}
|
|
88
|
+
href={`#${item.id}`}
|
|
89
|
+
onClick={(e) => handleClick(e, item.id)}
|
|
90
|
+
className={`block text-sm transition-all cursor-pointer rounded-xl px-3 py-2 ${item.level === 3 ? "ml-3" : ""} ${
|
|
91
|
+
activeId === item.id
|
|
92
|
+
? "text-primary font-medium"
|
|
93
|
+
: "text-foreground hover:bg-accent/50"
|
|
94
|
+
}`}
|
|
95
|
+
>
|
|
96
|
+
{item.title}
|
|
97
|
+
</a>
|
|
98
|
+
))}
|
|
99
|
+
</nav>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</aside>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, Children, isValidElement } from "react"
|
|
4
|
+
|
|
5
|
+
interface TabProps {
|
|
6
|
+
label: string
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TabsProps {
|
|
11
|
+
children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[]
|
|
12
|
+
defaultValue?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Tab({ children }: TabProps) {
|
|
16
|
+
return <>{children}</>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Tabs({ children, defaultValue }: TabsProps) {
|
|
20
|
+
const tabs = Children.toArray(children).filter(isValidElement) as React.ReactElement<TabProps>[]
|
|
21
|
+
|
|
22
|
+
// Use defaultValue or first tab label as initial active tab
|
|
23
|
+
const firstTabLabel = tabs[0]?.props.label || ""
|
|
24
|
+
const [activeTab, setActiveTab] = useState(defaultValue || firstTabLabel)
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="my-6">
|
|
28
|
+
{/* Tab buttons */}
|
|
29
|
+
<div className="flex items-center gap-1 border-b border-border mb-4">
|
|
30
|
+
{tabs.map((tab) => {
|
|
31
|
+
const label = tab.props.label
|
|
32
|
+
const isActive = activeTab === label
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={label}
|
|
37
|
+
onClick={() => setActiveTab(label)}
|
|
38
|
+
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
|
39
|
+
isActive
|
|
40
|
+
? "border-primary text-primary"
|
|
41
|
+
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
|
|
42
|
+
}`}
|
|
43
|
+
>
|
|
44
|
+
{label}
|
|
45
|
+
</button>
|
|
46
|
+
)
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Tab content */}
|
|
51
|
+
{tabs.map((tab) => {
|
|
52
|
+
const label = tab.props.label
|
|
53
|
+
if (activeTab !== label) return null
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div key={label} className="prose prose-slate dark:prose-invert max-w-none [&>*:first-child]:mt-0">
|
|
57
|
+
{tab.props.children}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Moon, Sun } from "lucide-react"
|
|
4
|
+
import { useEffect, useState } from "react"
|
|
5
|
+
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const [theme, setTheme] = useState<"light" | "dark">("dark")
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
// Check for saved theme preference or default to dark
|
|
11
|
+
const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null
|
|
12
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
13
|
+
const initialTheme = savedTheme || (prefersDark ? "dark" : "light")
|
|
14
|
+
|
|
15
|
+
setTheme(initialTheme)
|
|
16
|
+
document.documentElement.classList.toggle("dark", initialTheme === "dark")
|
|
17
|
+
}, [])
|
|
18
|
+
|
|
19
|
+
const toggleTheme = () => {
|
|
20
|
+
const newTheme = theme === "dark" ? "light" : "dark"
|
|
21
|
+
setTheme(newTheme)
|
|
22
|
+
localStorage.setItem("theme", newTheme)
|
|
23
|
+
document.documentElement.classList.toggle("dark", newTheme === "dark")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
onClick={toggleTheme}
|
|
29
|
+
className="flex items-center justify-center w-9 h-9 rounded-md border border-border bg-background hover:bg-accent transition-colors"
|
|
30
|
+
aria-label="Toggle theme"
|
|
31
|
+
>
|
|
32
|
+
{theme === "dark" ? (
|
|
33
|
+
<Sun className="h-4 w-4 text-foreground" />
|
|
34
|
+
) : (
|
|
35
|
+
<Moon className="h-4 w-4 text-foreground" />
|
|
36
|
+
)}
|
|
37
|
+
</button>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
interface TooltipProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
content: string
|
|
8
|
+
position?: "top" | "bottom" | "left" | "right"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Tooltip({ children, content, position = "top" }: TooltipProps) {
|
|
12
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
13
|
+
|
|
14
|
+
const positions = {
|
|
15
|
+
top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
|
|
16
|
+
bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
|
|
17
|
+
left: "right-full top-1/2 -translate-y-1/2 mr-2",
|
|
18
|
+
right: "left-full top-1/2 -translate-y-1/2 ml-2",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<span
|
|
23
|
+
className="relative inline-flex underline decoration-dotted cursor-help"
|
|
24
|
+
onMouseEnter={() => setIsVisible(true)}
|
|
25
|
+
onMouseLeave={() => setIsVisible(false)}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
{isVisible && (
|
|
29
|
+
<span
|
|
30
|
+
className={`absolute ${positions[position]} z-50 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded whitespace-nowrap pointer-events-none`}
|
|
31
|
+
>
|
|
32
|
+
{content}
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
</span>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Check, ChevronDown } from "lucide-react"
|
|
5
|
+
import { useRouter } from "next/navigation"
|
|
6
|
+
|
|
7
|
+
interface VersionSwitcherProps {
|
|
8
|
+
currentVersion: string
|
|
9
|
+
versions: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function VersionSwitcher({ currentVersion, versions }: VersionSwitcherProps) {
|
|
13
|
+
const [open, setOpen] = useState(false)
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
|
|
16
|
+
const handleVersionChange = (version: string) => {
|
|
17
|
+
router.push(`/docs/${version}`)
|
|
18
|
+
setOpen(false)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="relative">
|
|
23
|
+
<button
|
|
24
|
+
onClick={() => setOpen(!open)}
|
|
25
|
+
className="flex items-center gap-2 px-3 py-2 text-sm text-foreground bg-muted rounded-md hover:bg-muted/80 transition-colors"
|
|
26
|
+
>
|
|
27
|
+
<span className="font-medium">{currentVersion}</span>
|
|
28
|
+
<ChevronDown className="h-4 w-4" />
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
{open && (
|
|
32
|
+
<>
|
|
33
|
+
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
34
|
+
<div className="absolute right-0 mt-2 w-48 bg-background border border-border rounded-md shadow-lg z-50">
|
|
35
|
+
<div className="p-2">
|
|
36
|
+
{versions.map((version) => (
|
|
37
|
+
<button
|
|
38
|
+
key={version}
|
|
39
|
+
onClick={() => handleVersionChange(version)}
|
|
40
|
+
className="flex items-center justify-between w-full px-3 py-2 text-sm text-foreground hover:bg-muted rounded-md transition-colors"
|
|
41
|
+
>
|
|
42
|
+
<span>{version}</span>
|
|
43
|
+
{version === currentVersion && <Check className="h-4 w-4 text-primary" />}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
interface VideoProps {
|
|
4
|
+
src: string
|
|
5
|
+
caption?: string
|
|
6
|
+
autoplay?: boolean
|
|
7
|
+
loop?: boolean
|
|
8
|
+
muted?: boolean
|
|
9
|
+
controls?: boolean
|
|
10
|
+
poster?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Video({
|
|
14
|
+
src,
|
|
15
|
+
caption,
|
|
16
|
+
autoplay = false,
|
|
17
|
+
loop = false,
|
|
18
|
+
muted = false,
|
|
19
|
+
controls = true,
|
|
20
|
+
poster,
|
|
21
|
+
}: VideoProps) {
|
|
22
|
+
// Check if it's a YouTube or Vimeo URL
|
|
23
|
+
const isYouTube = src.includes("youtube.com") || src.includes("youtu.be")
|
|
24
|
+
const isVimeo = src.includes("vimeo.com")
|
|
25
|
+
|
|
26
|
+
const getYouTubeId = (url: string) => {
|
|
27
|
+
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)
|
|
28
|
+
return match ? match[1] : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getVimeoId = (url: string) => {
|
|
32
|
+
const match = url.match(/vimeo\.com\/(\d+)/)
|
|
33
|
+
return match ? match[1] : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<figure className="my-6">
|
|
38
|
+
<div className="relative rounded-xl border border-border overflow-hidden bg-muted/30">
|
|
39
|
+
{isYouTube ? (
|
|
40
|
+
<div className="relative w-full" style={{ paddingBottom: "56.25%" }}>
|
|
41
|
+
<iframe
|
|
42
|
+
className="absolute top-0 left-0 w-full h-full"
|
|
43
|
+
src={`https://www.youtube.com/embed/${getYouTubeId(src)}${autoplay ? "?autoplay=1" : ""}`}
|
|
44
|
+
title="YouTube video"
|
|
45
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
46
|
+
allowFullScreen
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
) : isVimeo ? (
|
|
50
|
+
<div className="relative w-full" style={{ paddingBottom: "56.25%" }}>
|
|
51
|
+
<iframe
|
|
52
|
+
className="absolute top-0 left-0 w-full h-full"
|
|
53
|
+
src={`https://player.vimeo.com/video/${getVimeoId(src)}${autoplay ? "?autoplay=1" : ""}`}
|
|
54
|
+
title="Vimeo video"
|
|
55
|
+
allow="autoplay; fullscreen; picture-in-picture"
|
|
56
|
+
allowFullScreen
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
) : (
|
|
60
|
+
<video
|
|
61
|
+
src={src}
|
|
62
|
+
controls={controls}
|
|
63
|
+
autoPlay={autoplay}
|
|
64
|
+
loop={loop}
|
|
65
|
+
muted={muted}
|
|
66
|
+
poster={poster}
|
|
67
|
+
className="w-full h-auto"
|
|
68
|
+
>
|
|
69
|
+
Your browser does not support the video tag.
|
|
70
|
+
</video>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
{caption && (
|
|
74
|
+
<figcaption className="mt-2 text-center text-sm text-muted-foreground italic">
|
|
75
|
+
{caption}
|
|
76
|
+
</figcaption>
|
|
77
|
+
)}
|
|
78
|
+
</figure>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AlertTriangle } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export function VersionNotFound() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
8
|
+
<div className="text-center">
|
|
9
|
+
<div className="mb-4 flex justify-center">
|
|
10
|
+
<AlertTriangle className="h-16 w-16 text-yellow-500" />
|
|
11
|
+
</div>
|
|
12
|
+
<h1 className="mb-2 text-4xl font-bold">Version Not Found</h1>
|
|
13
|
+
<p className="mb-6 text-muted-foreground">
|
|
14
|
+
The documentation version you're looking for doesn't exist.
|
|
15
|
+
</p>
|
|
16
|
+
<Link
|
|
17
|
+
href="/docs/v1.0.0"
|
|
18
|
+
className="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
19
|
+
>
|
|
20
|
+
Go to Latest Version
|
|
21
|
+
</Link>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider as NextThemesProvider,
|
|
6
|
+
type ThemeProviderProps,
|
|
7
|
+
} from 'next-themes'
|
|
8
|
+
|
|
9
|
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
10
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
11
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
14
|
+
secondary:
|
|
15
|
+
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
16
|
+
destructive:
|
|
17
|
+
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
18
|
+
outline:
|
|
19
|
+
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"span"> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : "span"
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
data-slot="badge"
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
13
|
+
destructive:
|
|
14
|
+
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
15
|
+
outline:
|
|
16
|
+
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
17
|
+
secondary:
|
|
18
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
19
|
+
ghost:
|
|
20
|
+
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
25
|
+
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
26
|
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
27
|
+
icon: 'size-9',
|
|
28
|
+
'icon-sm': 'size-8',
|
|
29
|
+
'icon-lg': 'size-10',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
size: 'default',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<'button'> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : 'button'
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants }
|