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,29 @@
1
+ import siteConfig from "@/camelmind.config"
2
+ import type { AuthConfig, CamelMindConfig } from "./config-types"
3
+
4
+ export type { AuthConfig, AuthProvider, CamelMindConfig, OidcConfig } from "./config-types"
5
+
6
+ let _config: CamelMindConfig | null = null
7
+
8
+ export function getConfig(): CamelMindConfig {
9
+ if (!_config) _config = siteConfig
10
+ return _config
11
+ }
12
+
13
+ export function isAuthEnabled(): boolean {
14
+ if (process.env.OFFLINE_MODE === "true") return false
15
+ return getConfig().auth.enabled
16
+ }
17
+
18
+ export function isPrivateSite(): boolean {
19
+ return isAuthEnabled() && getConfig().auth.requireLogin
20
+ }
21
+
22
+ export function getAuthConfig(): AuthConfig {
23
+ return getConfig().auth
24
+ }
25
+
26
+ export function isPublicPath(pathname: string): boolean {
27
+ const { publicPaths } = getAuthConfig()
28
+ return publicPaths.some((p) => pathname === p || pathname.startsWith(p + "/"))
29
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import matter from "gray-matter"
4
+
5
+ export type FrontMatter = {
6
+ title: string
7
+ description?: string
8
+ roles?: string[]
9
+ tags?: string[]
10
+ download_pdf?: string
11
+ }
12
+
13
+ export type TocEntry = {
14
+ id: string
15
+ text: string
16
+ level: number
17
+ }
18
+
19
+ export type DocContent = {
20
+ frontmatter: FrontMatter
21
+ source: string
22
+ toc: TocEntry[]
23
+ }
24
+
25
+ function extractToc(source: string): TocEntry[] {
26
+ const headingRegex = /^(#{2,3})\s+(.+)$/gm
27
+ const entries: TocEntry[] = []
28
+ let match
29
+
30
+ while ((match = headingRegex.exec(source)) !== null) {
31
+ const level = match[1].length
32
+ const text = match[2].trim()
33
+ const id = text
34
+ .toLowerCase()
35
+ .replace(/[^\w\s-]/g, "")
36
+ .replace(/\s+/g, "-")
37
+ entries.push({ id, text, level })
38
+ }
39
+
40
+ return entries
41
+ }
42
+
43
+ export function loadMdxFile(filePath: string): DocContent {
44
+ const fullPath = path.join(process.cwd(), filePath)
45
+ const raw = fs.readFileSync(fullPath, "utf-8")
46
+ const { data, content } = matter(raw)
47
+
48
+ return {
49
+ frontmatter: data as FrontMatter,
50
+ source: content,
51
+ toc: extractToc(content),
52
+ }
53
+ }
@@ -0,0 +1,31 @@
1
+ export type NavChild = {
2
+ label: string
3
+ slug: string
4
+ file: string
5
+ roles: string[]
6
+ children?: NavChild[]
7
+ }
8
+
9
+ export type NavEntry = {
10
+ label: string
11
+ slug: string
12
+ file: string
13
+ roles: string[]
14
+ section?: NavChild[]
15
+ }
16
+
17
+ export type NavGroup = {
18
+ label: string
19
+ dropdown: boolean
20
+ noDropdown?: boolean // show as direct link in top nav, not a dropdown button
21
+ slug?: string // href for the direct link when noDropdown is true
22
+ items: NavEntry[]
23
+ }
24
+
25
+ export type NavConfig = {
26
+ nav: (NavEntry | NavGroup)[]
27
+ }
28
+
29
+ export function isNavGroup(item: NavEntry | NavGroup): item is NavGroup {
30
+ return "dropdown" in item
31
+ }
@@ -0,0 +1,125 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import yaml from "js-yaml"
4
+ import type { NavConfig, NavEntry, NavGroup, NavChild } from "./nav-types"
5
+
6
+ export type { NavConfig, NavEntry, NavGroup, NavChild }
7
+ export { isNavGroup } from "./nav-types"
8
+
9
+ let _navCache: NavConfig | null = null
10
+
11
+ export function loadNav(): NavConfig {
12
+ if (process.env.NODE_ENV !== "development" && _navCache) return _navCache
13
+ const navPath = path.join(process.cwd(), "nav", "nav.yml")
14
+ const raw = fs.readFileSync(navPath, "utf-8")
15
+ _navCache = yaml.load(raw) as NavConfig
16
+ return _navCache
17
+ }
18
+
19
+ export function flattenChildren(children: NavChild[]): NavChild[] {
20
+ return children.flatMap((c) => [c, ...(c.children ? flattenChildren(c.children) : [])])
21
+ }
22
+
23
+ // Extract all slugs from any NavConfig (default or versioned)
24
+ export function getSlugsFromConfig(nav: NavConfig): string[] {
25
+ const slugs: string[] = []
26
+ for (const item of nav.nav) {
27
+ if ("dropdown" in item) {
28
+ const group = item as NavGroup
29
+ // noDropdown groups have a top-level slug (the direct link target)
30
+ if (group.noDropdown && group.slug) slugs.push(group.slug)
31
+ for (const entry of (group.items ?? [])) {
32
+ slugs.push(entry.slug)
33
+ if (entry.section) {
34
+ for (const child of flattenChildren(entry.section)) {
35
+ slugs.push(child.slug)
36
+ }
37
+ }
38
+ }
39
+ } else if ("slug" in item) {
40
+ slugs.push((item as NavEntry).slug)
41
+ }
42
+ }
43
+ return slugs
44
+ }
45
+
46
+ export function getAllSlugs(): string[] {
47
+ return getSlugsFromConfig(loadNav())
48
+ }
49
+
50
+ // Search any NavConfig for a slug — used for both default and versioned navs
51
+ export function getEntryBySlugFromConfig(nav: NavConfig, slug: string): (NavEntry | NavChild) | null {
52
+ const normalized = slug.startsWith("/") ? slug : `/${slug}`
53
+
54
+ for (const item of nav.nav) {
55
+ if ("dropdown" in item) {
56
+ const group = item as NavGroup
57
+ // noDropdown group: its top-level slug maps to the first item in the group
58
+ if (group.noDropdown && group.slug === normalized && group.items?.length) {
59
+ return group.items[0]
60
+ }
61
+ for (const entry of (group.items ?? [])) {
62
+ if (entry.slug === normalized) return entry
63
+ if (entry.section) {
64
+ const match = flattenChildren(entry.section).find((c) => c.slug === normalized)
65
+ if (match) return match
66
+ }
67
+ }
68
+ } else if ("slug" in item) {
69
+ const entry = item as NavEntry
70
+ if (entry.slug === normalized) return entry
71
+ }
72
+ }
73
+
74
+ return null
75
+ }
76
+
77
+ export function getNavEntryBySlug(slug: string): (NavEntry | NavChild) | null {
78
+ return getEntryBySlugFromConfig(loadNav(), slug)
79
+ }
80
+
81
+ // Return the NavGroup that contains the given slug in any NavConfig
82
+ export function getGroupForSlugFromConfig(nav: NavConfig, slug: string): NavGroup | null {
83
+ const normalized = slug.startsWith("/") ? slug : `/${slug}`
84
+
85
+ for (const item of nav.nav) {
86
+ if (!("dropdown" in item)) continue
87
+ const group = item as NavGroup
88
+ if (group.noDropdown && group.slug === normalized) return group
89
+ for (const entry of (group.items ?? [])) {
90
+ if (entry.slug === normalized) return group
91
+ if (entry.section) {
92
+ const match = flattenChildren(entry.section).find((c) => c.slug === normalized)
93
+ if (match) return group
94
+ }
95
+ }
96
+ }
97
+
98
+ return null
99
+ }
100
+
101
+ export function getGroupForSlug(slug: string): NavGroup | null {
102
+ return getGroupForSlugFromConfig(loadNav(), slug)
103
+ }
104
+
105
+ // Return the section-root NavEntry for a slug in any NavConfig
106
+ export function getSectionForSlugFromConfig(nav: NavConfig, slug: string): NavEntry | null {
107
+ const normalized = slug.startsWith("/") ? slug : `/${slug}`
108
+
109
+ for (const item of nav.nav) {
110
+ if (!("dropdown" in item)) continue
111
+ for (const entry of ((item as NavGroup).items ?? [])) {
112
+ if (entry.slug === normalized) return entry
113
+ if (entry.section) {
114
+ const match = flattenChildren(entry.section).find((c) => c.slug === normalized)
115
+ if (match) return entry
116
+ }
117
+ }
118
+ }
119
+
120
+ return null
121
+ }
122
+
123
+ export function getSectionForSlug(slug: string): NavEntry | null {
124
+ return getSectionForSlugFromConfig(loadNav(), slug)
125
+ }
@@ -0,0 +1,61 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import yaml from "js-yaml"
4
+ import { loadNav } from "./nav"
5
+ import type { NavConfig } from "./nav-types"
6
+
7
+ export type Version = {
8
+ id: string
9
+ label: string
10
+ stable: boolean
11
+ badge?: string
12
+ nav: string
13
+ }
14
+
15
+ export type VersionsConfig = {
16
+ versions: Version[]
17
+ }
18
+
19
+ let _versionsCache: VersionsConfig | null = null
20
+
21
+ export function loadVersions(): VersionsConfig {
22
+ if (_versionsCache) return _versionsCache
23
+ const raw = fs.readFileSync(path.join(process.cwd(), "versions.yml"), "utf-8")
24
+ _versionsCache = yaml.load(raw) as VersionsConfig
25
+ return _versionsCache
26
+ }
27
+
28
+ export function getVersionFromSlug(slug: string): string | null {
29
+ const { versions } = loadVersions()
30
+ for (const v of versions) {
31
+ if (slug.startsWith(`/${v.id}/`) || slug === `/${v.id}`) return v.id
32
+ }
33
+ // no version prefix = default (latest)
34
+ return null
35
+ }
36
+
37
+ export function getNavForVersion(versionId: string | null): NavConfig {
38
+ if (!versionId) return loadNav()
39
+ const { versions } = loadVersions()
40
+ const v = versions.find((v) => v.id === versionId)
41
+ if (!v) return loadNav()
42
+
43
+ const raw = fs.readFileSync(path.join(process.cwd(), v.nav), "utf-8")
44
+ return yaml.load(raw) as NavConfig
45
+ }
46
+
47
+ export function getLatestVersion(): Version {
48
+ const { versions } = loadVersions()
49
+ return versions[0]
50
+ }
51
+
52
+ // Given a slug like /v1.9/getting-started/overview, return /getting-started/overview
53
+ export function stripVersionPrefix(slug: string, versionId: string): string {
54
+ return slug.replace(new RegExp(`^/${versionId}`), "") || "/"
55
+ }
56
+
57
+ // Given /getting-started/overview and a target version, return /v1.9/getting-started/overview
58
+ export function swapVersion(slug: string, currentVersionId: string | null, targetVersionId: string): string {
59
+ const bare = currentVersionId ? stripVersionPrefix(slug, currentVersionId) : slug
60
+ return `/${targetVersionId}${bare}`
61
+ }
@@ -0,0 +1,20 @@
1
+ nav:
2
+ - label: "Getting Started"
3
+ dropdown: true
4
+ items:
5
+ - label: "Overview"
6
+ slug: /getting-started/overview
7
+ file: content/getting-started/overview.mdx
8
+ roles: []
9
+ - label: "Installation"
10
+ slug: /getting-started/installation
11
+ file: content/getting-started/installation.mdx
12
+ roles: []
13
+
14
+ - label: "Guides"
15
+ dropdown: true
16
+ items:
17
+ - label: "Writing Docs"
18
+ slug: /guides/writing-docs
19
+ file: content/guides/writing-docs.mdx
20
+ roles: []
@@ -0,0 +1,35 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const isOffline = process.env.OFFLINE_MODE === "true"
4
+
5
+ const securityHeaders = [
6
+ { key: "X-Frame-Options", value: "DENY" },
7
+ { key: "X-Content-Type-Options", value: "nosniff" },
8
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
9
+ { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
10
+ {
11
+ key: "Content-Security-Policy",
12
+ value: [
13
+ "default-src 'self'",
14
+ "script-src 'self' 'unsafe-inline'",
15
+ "style-src 'self' 'unsafe-inline'",
16
+ "img-src 'self' data:",
17
+ "font-src 'self'",
18
+ "connect-src 'self'",
19
+ "frame-ancestors 'none'",
20
+ ].join("; "),
21
+ },
22
+ ]
23
+
24
+ const nextConfig: NextConfig = {
25
+ // Static export for offline builds — no server required
26
+ ...(isOffline ? { output: "export", trailingSlash: true } : {}),
27
+ // Security headers are only meaningful when running as a server (not static export)
28
+ ...(!isOffline ? {
29
+ async headers() {
30
+ return [{ source: "/(.*)", headers: securityHeaders }]
31
+ },
32
+ } : {}),
33
+ };
34
+
35
+ export default nextConfig;
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,50 @@
1
+ import { NextRequest, NextResponse } from "next/server"
2
+ import { jwtVerify } from "jose"
3
+ import { COOKIE_NAME, getSecret } from "@/lib/auth"
4
+ import { isAuthEnabled, isPrivateSite, isPublicPath } from "@/lib/config"
5
+
6
+ export async function proxy(req: NextRequest) {
7
+ const { pathname } = req.nextUrl
8
+
9
+ // Auth disabled — all traffic passes through
10
+ if (!isAuthEnabled()) {
11
+ return NextResponse.next()
12
+ }
13
+
14
+ // Always allow public paths and Next.js internals
15
+ if (
16
+ isPublicPath(pathname) ||
17
+ pathname.startsWith("/_next") ||
18
+ pathname.startsWith("/favicon")
19
+ ) {
20
+ return NextResponse.next()
21
+ }
22
+
23
+ // RBAC-only mode: per-page enforcement happens in page.tsx
24
+ if (!isPrivateSite()) {
25
+ return NextResponse.next()
26
+ }
27
+
28
+ // Private site mode: require valid session for all non-public paths
29
+ const token = req.cookies.get(COOKIE_NAME)?.value
30
+ if (!token) {
31
+ const loginUrl = new URL("/login", req.url)
32
+ loginUrl.searchParams.set("returnTo", pathname)
33
+ return NextResponse.redirect(loginUrl)
34
+ }
35
+
36
+ try {
37
+ await jwtVerify(token, getSecret())
38
+ return NextResponse.next()
39
+ } catch {
40
+ const loginUrl = new URL("/login", req.url)
41
+ loginUrl.searchParams.set("returnTo", pathname)
42
+ const res = NextResponse.redirect(loginUrl)
43
+ res.cookies.set(COOKIE_NAME, "", { maxAge: 0, path: "/" })
44
+ return res
45
+ }
46
+ }
47
+
48
+ export const config = {
49
+ matcher: ["/((?!_next/static|_next/image|favicon.ico|public/).*)"],
50
+ }
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env bash
2
+ # Build a self-contained offline ZIP for one version of the help center.
3
+ #
4
+ # Usage:
5
+ # ./scripts/build-offline.sh v2.0
6
+ # ./scripts/build-offline.sh v1.9
7
+ #
8
+ # Output: offline-builds/gw-helpcenter-<version>-offline.zip
9
+ # Users unzip and run: npx serve out/ (or open index.html directly)
10
+
11
+ set -euo pipefail
12
+
13
+ VERSION=${1:-v2.0}
14
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15
+ OUT_DIR="$ROOT/out"
16
+ BUILDS_DIR="$ROOT/offline-builds"
17
+ ZIP_NAME="camelmind-${VERSION}-offline.zip"
18
+
19
+ # API routes that require a live server — stash them outside app/ during static export
20
+ SERVER_ROUTES=(
21
+ "app/api/raw/route.ts"
22
+ "app/api/download/route.ts"
23
+ "app/api/search/route.ts"
24
+ "app/api/auth"
25
+ )
26
+
27
+ stash_routes() {
28
+ mkdir -p "$ROOT/.offline-stash"
29
+ for r in "${SERVER_ROUTES[@]}"; do
30
+ if [ -e "$ROOT/$r" ]; then
31
+ # Flatten path for stash filename
32
+ stash_name="${r//\//__}"
33
+ mv "$ROOT/$r" "$ROOT/.offline-stash/$stash_name"
34
+ fi
35
+ done
36
+ }
37
+
38
+ restore_routes() {
39
+ for r in "${SERVER_ROUTES[@]}"; do
40
+ stash_name="${r//\//__}"
41
+ if [ -e "$ROOT/.offline-stash/$stash_name" ]; then
42
+ # Recreate parent dir if needed (e.g. app/api/raw/)
43
+ mkdir -p "$ROOT/$(dirname "$r")"
44
+ mv "$ROOT/.offline-stash/$stash_name" "$ROOT/$r"
45
+ fi
46
+ done
47
+ rmdir "$ROOT/.offline-stash" 2>/dev/null || true
48
+ }
49
+
50
+ # Always restore on exit (even on error)
51
+ trap restore_routes EXIT
52
+
53
+ echo "▶ Building offline package for version: $VERSION"
54
+
55
+ # 1. Pre-build the search index (no API server in static export)
56
+ echo " → Generating search index..."
57
+ cd "$ROOT"
58
+ npx tsx scripts/build-search-index.ts
59
+
60
+ # 2. Stash server-only routes so next build doesn't complain
61
+ echo " → Stashing server-only API routes..."
62
+ stash_routes
63
+
64
+ # 3. Static export
65
+ echo " → Running next build (OFFLINE_MODE=true, TARGET_VERSION=$VERSION)..."
66
+ OFFLINE_MODE=true TARGET_VERSION="$VERSION" npx next build
67
+
68
+ # 3. Write launcher scripts and README into the export
69
+
70
+ cat > "$OUT_DIR/launch.sh" <<'LAUNCHER'
71
+ #!/usr/bin/env bash
72
+ # Game Warden Help Center — offline launcher (Mac / Linux)
73
+ PORT=8765
74
+ URL="http://localhost:$PORT/home/"
75
+
76
+ # Try Python 3 first (available on most systems without internet)
77
+ if command -v python3 &>/dev/null; then
78
+ echo "Starting local server on $URL"
79
+ python3 -m http.server $PORT &
80
+ SERVER_PID=$!
81
+ sleep 1
82
+ # Open browser
83
+ if command -v open &>/dev/null; then open "$URL"
84
+ elif command -v xdg-open &>/dev/null; then xdg-open "$URL"
85
+ fi
86
+ echo "Press Ctrl+C to stop the server."
87
+ wait $SERVER_PID
88
+ # Fall back to Python 2
89
+ elif command -v python &>/dev/null; then
90
+ echo "Starting local server on $URL"
91
+ python -m SimpleHTTPServer $PORT &
92
+ SERVER_PID=$!
93
+ sleep 1
94
+ if command -v open &>/dev/null; then open "$URL"
95
+ elif command -v xdg-open &>/dev/null; then xdg-open "$URL"
96
+ fi
97
+ echo "Press Ctrl+C to stop the server."
98
+ wait $SERVER_PID
99
+ # Fall back to Node / npx
100
+ elif command -v npx &>/dev/null; then
101
+ echo "Starting local server on $URL"
102
+ if command -v open &>/dev/null; then sleep 2 && open "$URL" &
103
+ elif command -v xdg-open &>/dev/null; then sleep 2 && xdg-open "$URL" &
104
+ fi
105
+ npx serve . -p $PORT
106
+ else
107
+ echo "ERROR: Python 3, Python 2, or Node.js is required to run this package."
108
+ echo "Install Python from https://www.python.org or Node.js from https://nodejs.org"
109
+ exit 1
110
+ fi
111
+ LAUNCHER
112
+ chmod +x "$OUT_DIR/launch.sh"
113
+
114
+ cat > "$OUT_DIR/launch.bat" <<'LAUNCHER'
115
+ @echo off
116
+ REM Game Warden Help Center — offline launcher (Windows)
117
+ set PORT=8765
118
+ set URL=http://localhost:%PORT%/home/
119
+
120
+ REM Try Python 3
121
+ python --version >nul 2>&1
122
+ if %errorlevel% == 0 (
123
+ echo Starting local server on %URL%
124
+ start "" "%URL%"
125
+ python -m http.server %PORT%
126
+ goto :end
127
+ )
128
+
129
+ REM Try Python via py launcher
130
+ py --version >nul 2>&1
131
+ if %errorlevel% == 0 (
132
+ echo Starting local server on %URL%
133
+ start "" "%URL%"
134
+ py -m http.server %PORT%
135
+ goto :end
136
+ )
137
+
138
+ REM Try npx (Node.js)
139
+ npx --version >nul 2>&1
140
+ if %errorlevel% == 0 (
141
+ echo Starting local server on %URL%
142
+ start "" "%URL%"
143
+ npx serve . -p %PORT%
144
+ goto :end
145
+ )
146
+
147
+ echo ERROR: Python or Node.js is required to run this package.
148
+ echo Install Python from https://www.python.org or Node.js from https://nodejs.org
149
+ pause
150
+ :end
151
+ LAUNCHER
152
+
153
+ cat > "$OUT_DIR/README.txt" <<EOF
154
+ Game Warden Help Center — Offline Package ($VERSION)
155
+ ====================================================
156
+
157
+ QUICK START
158
+ -----------
159
+ Mac / Linux: Double-click launch.sh (or run: ./launch.sh in Terminal)
160
+ Windows: Double-click launch.bat
161
+
162
+ This opens a local web server at http://localhost:8765 and launches your
163
+ browser automatically. Press Ctrl+C (or close Terminal) to stop.
164
+
165
+ REQUIREMENTS
166
+ ------------
167
+ Python 3 is recommended (pre-installed on most Macs and Linux systems).
168
+ Windows users may need to install Python from https://www.python.org
169
+ Node.js (https://nodejs.org) works as a fallback on all platforms.
170
+
171
+ SEARCH
172
+ ------
173
+ Full-text search works offline — no internet connection required.
174
+
175
+ NOTES
176
+ -----
177
+ - Do not open index.html directly in your browser; navigation will break.
178
+ Always use the launcher or start a local server first.
179
+ - This package was generated for authenticated Game Warden users.
180
+ Do not redistribute without authorization.
181
+ EOF
182
+
183
+ # 4. Zip the output
184
+ echo " → Packaging..."
185
+ mkdir -p "$BUILDS_DIR"
186
+ cd "$OUT_DIR"
187
+ zip -r "$BUILDS_DIR/$ZIP_NAME" . --quiet
188
+
189
+ echo "✓ Done: offline-builds/$ZIP_NAME ($(du -sh "$BUILDS_DIR/$ZIP_NAME" | cut -f1))"
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # Build the master PDF for one version of the Help Center.
3
+ #
4
+ # Usage:
5
+ # ./scripts/build-pdf.sh v2.0
6
+ #
7
+ # Requires: the Next.js dev server running on port 3000 (npm run dev)
8
+ # Output: offline-builds/Game-Warden-Help-Center-<version>.pdf
9
+
10
+ set -euo pipefail
11
+
12
+ VERSION=${1:-v2.0}
13
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
14
+ PAGES_DIR="$ROOT/.pdf-pages"
15
+ PORT=3000
16
+
17
+ cleanup() {
18
+ rm -rf "$PAGES_DIR"
19
+ }
20
+ trap cleanup EXIT
21
+
22
+ echo "▶ Building master PDF for version: $VERSION"
23
+
24
+ # Verify dev server is running
25
+ if ! curl -s -o /dev/null "http://localhost:$PORT/home"; then
26
+ echo "✗ Dev server not running on port $PORT"
27
+ echo " Start it with: npm run dev"
28
+ exit 1
29
+ fi
30
+
31
+ # Generate per-page PDFs against the live dev server
32
+ echo " → Rendering pages via Playwright (port $PORT)..."
33
+ cd "$ROOT"
34
+ npx tsx scripts/generate-pdfs.ts --port="$PORT" --out="$PAGES_DIR"
35
+
36
+ # Assemble master PDF
37
+ echo " → Assembling master PDF..."
38
+ npx tsx scripts/generate-master-pdf.ts --version="$VERSION" --pages="$PAGES_DIR" --out="$ROOT/offline-builds"
39
+
40
+ SIZE=$(du -sh "$ROOT/offline-builds/Game-Warden-Help-Center-${VERSION}.pdf" | cut -f1)
41
+ echo "✓ Done: offline-builds/Game-Warden-Help-Center-${VERSION}.pdf ($SIZE)"