camelmind 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 (73) hide show
  1. package/dist/index.js +142 -0
  2. package/package.json +51 -0
  3. package/template/_env.example +16 -0
  4. package/template/_gitignore +32 -0
  5. package/template/_package.json +39 -0
  6. package/template/app/[...slug]/page.tsx +150 -0
  7. package/template/app/api/auth/callback/route.ts +47 -0
  8. package/template/app/api/auth/login/route.ts +39 -0
  9. package/template/app/api/auth/logout/route.ts +8 -0
  10. package/template/app/api/auth/signin/route.ts +64 -0
  11. package/template/app/api/download/route.ts +67 -0
  12. package/template/app/api/raw/route.ts +43 -0
  13. package/template/app/api/search/route.ts +138 -0
  14. package/template/app/favicon.ico +0 -0
  15. package/template/app/globals.css +524 -0
  16. package/template/app/home/page.tsx +136 -0
  17. package/template/app/icon.svg +29 -0
  18. package/template/app/layout.tsx +44 -0
  19. package/template/app/login/LoginForm.tsx +105 -0
  20. package/template/app/login/page.tsx +9 -0
  21. package/template/app/page.tsx +5 -0
  22. package/template/camelmind.config.ts +33 -0
  23. package/template/components/Breadcrumbs/Breadcrumbs.tsx +41 -0
  24. package/template/components/Code/CodeBlock.tsx +55 -0
  25. package/template/components/DocActions/DocActions.tsx +62 -0
  26. package/template/components/Nav/MobileDrawer.tsx +221 -0
  27. package/template/components/Nav/ThemeToggle.tsx +62 -0
  28. package/template/components/Nav/TopNav.tsx +173 -0
  29. package/template/components/Nav/VersionSelector.tsx +107 -0
  30. package/template/components/PageNav/PageNav.tsx +68 -0
  31. package/template/components/Search/SearchModal.tsx +278 -0
  32. package/template/components/SectionCards/SectionCards.tsx +33 -0
  33. package/template/components/Sidebar/Sidebar.tsx +196 -0
  34. package/template/components/Toc/Toc.tsx +57 -0
  35. package/template/components/ZoomImages/ZoomImages.tsx +24 -0
  36. package/template/components/mdx/Callout.tsx +43 -0
  37. package/template/components/mdx/Details.tsx +65 -0
  38. package/template/components/mdx/Icon.tsx +25 -0
  39. package/template/components/mdx/Steps.tsx +33 -0
  40. package/template/components/mdx/Tabs.tsx +69 -0
  41. package/template/components/mdx/index.tsx +34 -0
  42. package/template/content/getting-started/installation.mdx +51 -0
  43. package/template/content/getting-started/overview.mdx +39 -0
  44. package/template/content/guides/writing-docs.mdx +72 -0
  45. package/template/eslint.config.mjs +18 -0
  46. package/template/lib/auth-providers/dev-mock.ts +45 -0
  47. package/template/lib/auth-providers/oidc.ts +95 -0
  48. package/template/lib/auth-roles.ts +28 -0
  49. package/template/lib/auth.ts +76 -0
  50. package/template/lib/config-types.ts +30 -0
  51. package/template/lib/config.ts +29 -0
  52. package/template/lib/mdx.ts +53 -0
  53. package/template/lib/nav-types.ts +31 -0
  54. package/template/lib/nav.ts +125 -0
  55. package/template/lib/versions.ts +61 -0
  56. package/template/nav/nav.yml +20 -0
  57. package/template/next.config.ts +35 -0
  58. package/template/postcss.config.mjs +7 -0
  59. package/template/proxy.ts +50 -0
  60. package/template/public/2f-logo.png +0 -0
  61. package/template/public/favicon.png +0 -0
  62. package/template/public/file.svg +1 -0
  63. package/template/public/globe.svg +1 -0
  64. package/template/public/images/gwa-app-central-main.png +0 -0
  65. package/template/public/next.svg +1 -0
  66. package/template/public/window.svg +1 -0
  67. package/template/scripts/build-offline.sh +189 -0
  68. package/template/scripts/build-pdf.sh +41 -0
  69. package/template/scripts/build-search-index.ts +90 -0
  70. package/template/scripts/generate-master-pdf.ts +260 -0
  71. package/template/scripts/generate-pdfs.ts +185 -0
  72. package/template/tsconfig.json +34 -0
  73. package/template/versions.yml +5 -0
@@ -0,0 +1,136 @@
1
+ import Link from "next/link"
2
+ import { FileText, Search, Palette, Code, Rocket, BookOpen } from "lucide-react"
3
+ import { loadNav, getSlugsFromConfig } from "@/lib/nav"
4
+ import { loadVersions, getNavForVersion } from "@/lib/versions"
5
+ import { getSession } from "@/lib/auth"
6
+ import { getConfig, isAuthEnabled } from "@/lib/config"
7
+ import { TopNav } from "@/components/Nav/TopNav"
8
+
9
+ const FEATURE_CARDS = [
10
+ {
11
+ Icon: FileText,
12
+ title: "Markdown First",
13
+ description: "Write documentation in Markdown and MDX with a rich component library out of the box.",
14
+ href: "/guides/writing-docs",
15
+ cta: "Start writing",
16
+ },
17
+ {
18
+ Icon: Search,
19
+ title: "Instant Search",
20
+ description: "Built-in full-text search helps readers find answers quickly across your entire doc site.",
21
+ href: "/getting-started/overview",
22
+ cta: "Learn more",
23
+ },
24
+ {
25
+ Icon: Palette,
26
+ title: "Beautiful Themes",
27
+ description: "The Sahara theme ships with light and dark modes, tuned typography, and polished components.",
28
+ href: "/guides/mdx-components",
29
+ cta: "Explore components",
30
+ },
31
+ {
32
+ Icon: Code,
33
+ title: "Docs as Code",
34
+ description: "Keep documentation alongside your source code with YAML navigation and version support.",
35
+ href: "/guides/navigation",
36
+ cta: "Configure nav",
37
+ },
38
+ {
39
+ Icon: BookOpen,
40
+ title: "Role-Based Access",
41
+ description: "Optional SSO and RBAC let you gate internal pages while keeping public docs open.",
42
+ href: "/guides/auth-rbac",
43
+ cta: "Set up auth",
44
+ },
45
+ {
46
+ Icon: Rocket,
47
+ title: "Deploy Anywhere",
48
+ description: "Static export, Docker, or any Node.js host — publish your docs wherever you deploy.",
49
+ href: "/reference/configuration",
50
+ cta: "View config",
51
+ },
52
+ ]
53
+
54
+ export default async function HomePage() {
55
+ const config = getConfig()
56
+ const nav = loadNav()
57
+ const { versions } = loadVersions()
58
+ const session = await getSession()
59
+ const authEnabled = isAuthEnabled()
60
+ const versionSlugs = Object.fromEntries(
61
+ versions.map((v) => [v.id, getSlugsFromConfig(getNavForVersion(v.id))])
62
+ )
63
+
64
+ return (
65
+ <div className="flex flex-col min-h-screen">
66
+ <TopNav
67
+ nav={nav.nav}
68
+ userRoles={session?.roles ?? []}
69
+ userName={session?.name ?? null}
70
+ authEnabled={authEnabled}
71
+ versions={versions}
72
+ currentVersionId={null}
73
+ currentSlug="/home"
74
+ versionSlugs={versionSlugs}
75
+ />
76
+
77
+ <main className="flex-1 bg-[var(--cm-bg-primary)]">
78
+ <section className="bg-[var(--cm-night-dune)] text-[var(--cm-parchment)] px-4 md:px-6 py-14 md:py-20 text-center">
79
+ <p className="text-[var(--cm-sandstone)] text-sm font-semibold uppercase tracking-widest mb-3">Open Source</p>
80
+ <h1 className="text-4xl md:text-5xl font-bold mb-4 font-[family-name:var(--cm-font-heading)]">{config.title}</h1>
81
+ <p className="text-[var(--cm-sandstone)] text-lg max-w-xl mx-auto mb-8">{config.tagline}</p>
82
+ <p className="text-[var(--cm-text-muted)] text-base max-w-2xl mx-auto mb-8">
83
+ Build beautiful documentation from Markdown and MDX. Publish instantly, deploy anywhere, and keep docs close to your code.
84
+ </p>
85
+ <div className="flex flex-wrap justify-center gap-3">
86
+ <Link href="/getting-started/installation" className="cm-primary-button px-5 py-2.5 text-sm font-medium hover:opacity-90 transition-opacity">
87
+ Get Started
88
+ </Link>
89
+ {config.links.github && (
90
+ <a
91
+ href={config.links.github}
92
+ target="_blank"
93
+ rel="noopener noreferrer"
94
+ className="bg-transparent hover:bg-[var(--cm-bg-tertiary)] text-[var(--cm-parchment)] px-5 py-2.5 rounded-[var(--cm-radius-md)] text-sm font-medium transition-colors border border-[var(--cm-border)]"
95
+ >
96
+ View on GitHub
97
+ </a>
98
+ )}
99
+ </div>
100
+ </section>
101
+
102
+ <div className="max-w-5xl mx-auto px-4 md:px-6 py-10 md:py-16">
103
+ <section>
104
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
105
+ {FEATURE_CARDS.map((card) => (
106
+ <Link
107
+ key={card.href}
108
+ href={card.href}
109
+ className="group flex flex-col gap-4 p-7 rounded-xl border border-[var(--cm-border)] bg-[var(--cm-bg-secondary)] hover:border-[var(--cm-oasis-teal)] hover:shadow-md transition-all"
110
+ >
111
+ <card.Icon size={28} className="text-[var(--cm-oasis-teal)] shrink-0" />
112
+ <h3 className="text-lg font-semibold text-[var(--cm-text-primary)]">{card.title}</h3>
113
+ <p className="text-base text-[var(--cm-text-secondary)] leading-relaxed flex-1">{card.description}</p>
114
+ <span className="text-sm font-medium text-[var(--cm-oasis-teal)] group-hover:text-[var(--cm-link-hover)] transition-colors">
115
+ {card.cta} →
116
+ </span>
117
+ </Link>
118
+ ))}
119
+ </div>
120
+ </section>
121
+ </div>
122
+ </main>
123
+
124
+ <footer className="border-t border-[var(--cm-border)] px-6 py-6 bg-[var(--cm-bg-secondary)]">
125
+ <div className="max-w-5xl mx-auto flex items-center justify-between text-xs text-[var(--cm-text-muted)]">
126
+ <span>© {new Date().getFullYear()} CamelMind. MIT License.</span>
127
+ {config.links.github && (
128
+ <a href={config.links.github} target="_blank" rel="noopener noreferrer" className="hover:text-[var(--cm-oasis-teal)] transition-colors">
129
+ GitHub
130
+ </a>
131
+ )}
132
+ </div>
133
+ </footer>
134
+ </div>
135
+ )
136
+ }
@@ -0,0 +1,29 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <!-- Night Dune background -->
3
+ <rect width="32" height="32" rx="6" fill="#1E2833"/>
4
+
5
+ <!-- Camel silhouette in Camel Gold — overlapping shapes merge into one solid form -->
6
+
7
+ <!-- Body -->
8
+ <ellipse cx="18" cy="21" rx="10" ry="6" fill="#D8A15B"/>
9
+
10
+ <!-- Hump (rises above body, right of center) -->
11
+ <ellipse cx="21" cy="13" rx="7" ry="7" fill="#D8A15B"/>
12
+
13
+ <!-- Neck (connects head to body) -->
14
+ <rect x="5" y="15" width="6" height="8" fill="#D8A15B"/>
15
+
16
+ <!-- Head -->
17
+ <ellipse cx="8" cy="13" rx="4" ry="4" fill="#D8A15B"/>
18
+
19
+ <!-- Snout / muzzle -->
20
+ <ellipse cx="4.5" cy="16" rx="2.5" ry="2" fill="#D8A15B"/>
21
+
22
+ <!-- Front legs -->
23
+ <rect x="9" y="26" width="2.5" height="5" rx="1" fill="#D8A15B"/>
24
+ <rect x="14" y="26" width="2.5" height="5" rx="1" fill="#D8A15B"/>
25
+
26
+ <!-- Back legs -->
27
+ <rect x="20" y="26" width="2.5" height="5" rx="1" fill="#D8A15B"/>
28
+ <rect x="25" y="26" width="2.5" height="5" rx="1" fill="#D8A15B"/>
29
+ </svg>
@@ -0,0 +1,44 @@
1
+ import type { Metadata } from "next"
2
+ import { Alegreya, Inter, JetBrains_Mono } from "next/font/google"
3
+ import "./globals.css"
4
+ import { SearchModal } from "@/components/Search/SearchModal"
5
+ import { ThemeProvider } from "next-themes"
6
+ import { getConfig } from "@/lib/config"
7
+
8
+ const alegreya = Alegreya({
9
+ subsets: ["latin"],
10
+ variable: "--font-alegreya",
11
+ display: "swap",
12
+ })
13
+
14
+ const inter = Inter({
15
+ subsets: ["latin"],
16
+ variable: "--font-inter",
17
+ display: "swap",
18
+ })
19
+
20
+ const jetbrainsMono = JetBrains_Mono({
21
+ subsets: ["latin"],
22
+ variable: "--font-jetbrains-mono",
23
+ display: "swap",
24
+ })
25
+
26
+ const config = getConfig()
27
+
28
+ export const metadata: Metadata = {
29
+ title: config.title,
30
+ description: config.tagline,
31
+ }
32
+
33
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
34
+ return (
35
+ <html lang="en" className={`h-full antialiased ${alegreya.variable} ${inter.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
36
+ <body className="min-h-full flex flex-col bg-[var(--cm-bg-primary)] text-[var(--cm-text-primary)]">
37
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
38
+ {children}
39
+ <SearchModal />
40
+ </ThemeProvider>
41
+ </body>
42
+ </html>
43
+ )
44
+ }
@@ -0,0 +1,105 @@
1
+ "use client"
2
+
3
+ import { useState, Suspense } from "react"
4
+ import { useRouter, useSearchParams } from "next/navigation"
5
+ import { DEMO_PERSONAS } from "@/lib/auth-providers/dev-mock"
6
+ import type { AuthProvider } from "@/lib/config-types"
7
+
8
+ function LoginFormInner({ provider }: { provider: AuthProvider }) {
9
+ const router = useRouter()
10
+ const searchParams = useSearchParams()
11
+ const returnTo = searchParams.get("returnTo") ?? "/home"
12
+ const error = searchParams.get("error")
13
+ const [loading, setLoading] = useState(false)
14
+
15
+ async function signInWithOidc() {
16
+ setLoading(true)
17
+ window.location.href = `/api/auth/signin?returnTo=${encodeURIComponent(returnTo)}`
18
+ }
19
+
20
+ async function signInWithPersona(persona: string) {
21
+ setLoading(true)
22
+ const res = await fetch("/api/auth/login", {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({ persona, returnTo }),
26
+ })
27
+ const data = await res.json()
28
+ if (data.redirectTo) router.push(data.redirectTo)
29
+ else setLoading(false)
30
+ }
31
+
32
+ return (
33
+ <div className="min-h-screen bg-[var(--cm-night-dune)] flex items-center justify-center px-4">
34
+ <div className="bg-[var(--cm-bg-secondary)] border border-[var(--cm-border)] rounded-xl shadow-lg p-8 w-full max-w-md">
35
+ <div className="text-center mb-8">
36
+ <div className="inline-flex items-center gap-1 text-2xl mb-4">
37
+ <span className="text-[var(--cm-oasis-teal)] font-mono font-bold">&lt;</span>
38
+ <span className="text-[var(--cm-camel-gold)]">🐪</span>
39
+ <span className="text-[var(--cm-oasis-teal)] font-mono font-bold">&gt;</span>
40
+ </div>
41
+ <h1 className="text-2xl font-bold text-[var(--cm-text-primary)] font-[family-name:var(--cm-font-heading)]">CamelMind</h1>
42
+ <p className="text-[var(--cm-text-muted)] text-sm mt-1">Doc site spun up in minutes.</p>
43
+ </div>
44
+
45
+ {error && (
46
+ <p className="text-sm text-[var(--cm-danger)] bg-[var(--cm-danger-bg)] border border-[var(--cm-danger-border)] rounded-md px-3 py-2 mb-4">
47
+ Sign-in failed. Please try again.
48
+ </p>
49
+ )}
50
+
51
+ {provider === "oidc" ? (
52
+ <div className="space-y-4">
53
+ <p className="text-[var(--cm-text-secondary)] text-sm">
54
+ Sign in with your organization&apos;s identity provider to access restricted documentation.
55
+ </p>
56
+ <button
57
+ onClick={signInWithOidc}
58
+ disabled={loading}
59
+ className="w-full cm-primary-button px-4 py-2.5 text-sm font-medium transition-colors hover:opacity-90 disabled:opacity-50"
60
+ >
61
+ {loading ? "Redirecting…" : "Sign in with SSO"}
62
+ </button>
63
+ </div>
64
+ ) : (
65
+ <>
66
+ <div className="mb-6">
67
+ <p className="text-[var(--cm-text-primary)] font-medium text-sm mb-1">Development sign-in</p>
68
+ <p className="text-[var(--cm-text-muted)] text-xs">
69
+ Choose a persona to simulate role-based access. In production, set{" "}
70
+ <code className="text-[var(--cm-oasis-teal)]">auth.provider: &quot;oidc&quot;</code> in{" "}
71
+ <code className="text-[var(--cm-oasis-teal)]">camelmind.config.ts</code>.
72
+ </p>
73
+ </div>
74
+ <div className="space-y-3">
75
+ {DEMO_PERSONAS.map((persona) => (
76
+ <button
77
+ key={persona.id}
78
+ onClick={() => signInWithPersona(persona.id)}
79
+ disabled={loading}
80
+ className="w-full text-left bg-[var(--cm-bg-tertiary)] hover:bg-[var(--cm-dune-beige)] border border-[var(--cm-border)] rounded-lg px-4 py-3 transition-colors disabled:opacity-50"
81
+ >
82
+ <div className="flex items-center justify-between">
83
+ <span className="text-[var(--cm-text-primary)] text-sm font-medium">{persona.label}</span>
84
+ <span className="text-xs px-2 py-0.5 rounded border border-[var(--cm-border)] font-mono text-[var(--cm-text-muted)]">
85
+ {persona.badge}
86
+ </span>
87
+ </div>
88
+ <p className="text-[var(--cm-text-muted)] text-xs mt-0.5">{persona.description}</p>
89
+ </button>
90
+ ))}
91
+ </div>
92
+ </>
93
+ )}
94
+ </div>
95
+ </div>
96
+ )
97
+ }
98
+
99
+ export function LoginForm({ provider }: { provider: AuthProvider }) {
100
+ return (
101
+ <Suspense>
102
+ <LoginFormInner provider={provider} />
103
+ </Suspense>
104
+ )
105
+ }
@@ -0,0 +1,9 @@
1
+ import { redirect } from "next/navigation"
2
+ import { getAuthConfig, isAuthEnabled } from "@/lib/config"
3
+ import { LoginForm } from "./LoginForm"
4
+
5
+ export default function LoginPage() {
6
+ if (!isAuthEnabled()) redirect("/home")
7
+ const { provider } = getAuthConfig()
8
+ return <LoginForm provider={provider} />
9
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation"
2
+
3
+ export default function Home() {
4
+ redirect("/home")
5
+ }
@@ -0,0 +1,33 @@
1
+ import type { CamelMindConfig } from "./lib/config-types"
2
+
3
+ const config: CamelMindConfig = {
4
+ title: "My Docs",
5
+ tagline: "Documentation made simple.",
6
+ url: process.env.CAMELMIND_URL ?? "http://localhost:3000",
7
+
8
+ contentDir: "content",
9
+ navFile: "nav/nav.yml",
10
+ versionsFile: "versions.yml",
11
+
12
+ auth: {
13
+ enabled: process.env.CAMELMIND_AUTH_ENABLED === "true",
14
+ requireLogin: process.env.CAMELMIND_AUTH_REQUIRE_LOGIN === "true",
15
+ provider: (process.env.CAMELMIND_AUTH_PROVIDER as "dev-mock" | "oidc") ?? "dev-mock",
16
+
17
+ oidc: {
18
+ issuer: process.env.OIDC_ISSUER ?? "",
19
+ clientId: process.env.OIDC_CLIENT_ID ?? "",
20
+ clientSecret: process.env.OIDC_CLIENT_SECRET ?? "",
21
+ rolesClaim: process.env.OIDC_ROLES_CLAIM ?? "realm_access.roles",
22
+ roleMapping: {},
23
+ },
24
+
25
+ publicPaths: ["/", "/home", "/login", "/api/auth"],
26
+ },
27
+
28
+ links: {
29
+ github: "",
30
+ },
31
+ }
32
+
33
+ export default config
@@ -0,0 +1,41 @@
1
+ import Link from "next/link"
2
+ import type { NavGroup, NavEntry, NavChild } from "@/lib/nav-types"
3
+
4
+ type Crumb = { label: string; slug?: string }
5
+
6
+ type Props = {
7
+ activeGroup?: NavGroup | null
8
+ sectionEntry?: NavEntry | null
9
+ currentEntry?: NavEntry | NavChild | null
10
+ }
11
+
12
+ export function Breadcrumbs({ activeGroup, sectionEntry, currentEntry }: Props) {
13
+ const crumbs: Crumb[] = []
14
+
15
+ if (activeGroup) crumbs.push({ label: activeGroup.label })
16
+ if (sectionEntry && sectionEntry.slug !== currentEntry?.slug) {
17
+ crumbs.push({ label: sectionEntry.label, slug: sectionEntry.slug })
18
+ }
19
+ if (currentEntry) crumbs.push({ label: currentEntry.label })
20
+
21
+ if (crumbs.length <= 1) return null
22
+
23
+ return (
24
+ <nav className="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500 mb-4">
25
+ {crumbs.map((crumb, i) => (
26
+ <span key={i} className="flex items-center gap-1.5">
27
+ {i > 0 && <span>/</span>}
28
+ {crumb.slug ? (
29
+ <Link href={crumb.slug} className="hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
30
+ {crumb.label}
31
+ </Link>
32
+ ) : (
33
+ <span className={i === crumbs.length - 1 ? "text-gray-600 dark:text-gray-300 font-medium" : ""}>
34
+ {crumb.label}
35
+ </span>
36
+ )}
37
+ </span>
38
+ ))}
39
+ </nav>
40
+ )
41
+ }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ export function CodeBlock({ children, className }: { children: string; className?: string }) {
6
+ const [copied, setCopied] = useState(false)
7
+ const language = className?.replace("language-", "") ?? ""
8
+
9
+ const copy = () => {
10
+ navigator.clipboard.writeText(children)
11
+ setCopied(true)
12
+ setTimeout(() => setCopied(false), 2000)
13
+ }
14
+
15
+ return (
16
+ <div className="relative group my-4 rounded-lg overflow-hidden border border-gray-200">
17
+ {language && (
18
+ <div className="flex items-center justify-between px-4 py-2 border-b border-gray-700" style={{ backgroundColor: "#13151a" }}>
19
+ <span className="text-xs text-gray-400 font-mono">{language}</span>
20
+ <button
21
+ onClick={copy}
22
+ className="text-xs text-gray-400 hover:text-white transition-colors flex items-center gap-1"
23
+ >
24
+ {copied ? (
25
+ <>
26
+ <svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
28
+ </svg>
29
+ <span className="text-green-400">Copied</span>
30
+ </>
31
+ ) : (
32
+ <>
33
+ <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
35
+ </svg>
36
+ Copy
37
+ </>
38
+ )}
39
+ </button>
40
+ </div>
41
+ )}
42
+ {!language && (
43
+ <button
44
+ onClick={copy}
45
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 text-xs text-gray-400 hover:text-white bg-gray-800 rounded px-2 py-1 transition-all flex items-center gap-1"
46
+ >
47
+ {copied ? "Copied ✓" : "Copy"}
48
+ </button>
49
+ )}
50
+ <pre className="text-gray-100 p-4 overflow-x-auto text-sm leading-relaxed font-mono" style={{ backgroundColor: "#1a1d23" }}>
51
+ <code>{children}</code>
52
+ </pre>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,62 @@
1
+ "use client"
2
+
3
+ import { FileText, Download } from "lucide-react"
4
+
5
+ type Props = {
6
+ file: string
7
+ downloadPdf?: string
8
+ offline?: boolean
9
+ }
10
+
11
+ export function DocActions({ file, downloadPdf, offline }: Props) {
12
+ // In offline builds, /api/raw doesn't exist — only show PDF download if present
13
+ if (offline) {
14
+ if (!downloadPdf) return null
15
+ return (
16
+ <div className="flex items-center gap-5 mb-6 pb-5 border-b border-gray-200">
17
+ <a
18
+ href={downloadPdf}
19
+ download
20
+ className="flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors"
21
+ >
22
+ <Download size={14} />
23
+ Download PDF
24
+ </a>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ const rawBase = `/api/raw?file=${encodeURIComponent(file)}`
30
+
31
+ return (
32
+ <div className="flex items-center gap-5 mb-6 pb-5 border-b border-gray-200">
33
+ <a
34
+ href={rawBase}
35
+ target="_blank"
36
+ rel="noopener noreferrer"
37
+ className="flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors"
38
+ >
39
+ <FileText size={14} />
40
+ View as Markdown
41
+ </a>
42
+ <a
43
+ href={`${rawBase}&download=1`}
44
+ download
45
+ className="flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors"
46
+ >
47
+ <Download size={14} />
48
+ Download Markdown
49
+ </a>
50
+ {downloadPdf && (
51
+ <a
52
+ href={downloadPdf}
53
+ download
54
+ className="flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors"
55
+ >
56
+ <Download size={14} />
57
+ Download PDF
58
+ </a>
59
+ )}
60
+ </div>
61
+ )
62
+ }