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,90 @@
1
+ /**
2
+ * Generates public/search-index.json for offline builds.
3
+ * Run before `next build` when OFFLINE_MODE=true.
4
+ *
5
+ * Usage: npx tsx scripts/build-search-index.ts
6
+ */
7
+
8
+ import fs from "fs"
9
+ import path from "path"
10
+ import matter from "gray-matter"
11
+
12
+ const ROOT = path.resolve(process.cwd())
13
+ const NAV_FILE = path.join(ROOT, "nav", "nav.yml")
14
+ const OUT_FILE = path.join(ROOT, "public", "search-index.json")
15
+
16
+ // Minimal YAML parser for our nav structure (avoids pulling in the full Next.js lib graph)
17
+ import yaml from "js-yaml"
18
+
19
+ type NavChild = { label: string; slug: string; roles?: string[]; children?: NavChild[] }
20
+ type NavEntry = { label: string; slug: string; file?: string; roles?: string[]; section?: NavChild[] }
21
+ type NavGroup = { label: string; dropdown?: boolean; items?: NavEntry[] }
22
+
23
+ function collectEntries(nav: (NavEntry | NavGroup)[], group?: string): Array<{ slug: string; file: string; roles: string[]; group: string; section?: string }> {
24
+ const out: Array<{ slug: string; file: string; roles: string[]; group: string; section?: string }> = []
25
+
26
+ for (const item of nav) {
27
+ if ("items" in item && item.items) {
28
+ // NavGroup
29
+ for (const entry of item.items) {
30
+ if (entry.file) {
31
+ out.push({ slug: entry.slug, file: entry.file, roles: entry.roles ?? [], group: item.label })
32
+ }
33
+ for (const section of entry.section ?? []) {
34
+ if ((section as NavEntry).file) {
35
+ out.push({ slug: section.slug, file: (section as NavEntry).file!, roles: section.roles ?? [], group: item.label, section: entry.label })
36
+ }
37
+ for (const child of section.children ?? []) {
38
+ if ((child as NavEntry).file) {
39
+ out.push({ slug: child.slug, file: (child as NavEntry).file!, roles: child.roles ?? [], group: item.label, section: section.label })
40
+ }
41
+ }
42
+ }
43
+ }
44
+ } else {
45
+ const entry = item as NavEntry
46
+ if (entry.file) {
47
+ out.push({ slug: entry.slug, file: entry.file, roles: entry.roles ?? [], group: group ?? "" })
48
+ }
49
+ }
50
+ }
51
+
52
+ return out
53
+ }
54
+
55
+ function stripMdx(source: string): string {
56
+ return source
57
+ .replace(/<[^>]+>/g, " ")
58
+ .replace(/[#*`>|]/g, " ")
59
+ .replace(/\s+/g, " ")
60
+ .trim()
61
+ }
62
+
63
+ async function main() {
64
+ const raw = fs.readFileSync(NAV_FILE, "utf-8")
65
+ const nav = (yaml.load(raw) as { nav: (NavEntry | NavGroup)[] }).nav
66
+
67
+ const entries = collectEntries(nav)
68
+ const index = []
69
+
70
+ for (const entry of entries) {
71
+ const filePath = path.join(ROOT, entry.file)
72
+ if (!fs.existsSync(filePath)) continue
73
+ const { data: frontmatter, content } = matter(fs.readFileSync(filePath, "utf-8"))
74
+
75
+ index.push({
76
+ slug: entry.slug,
77
+ title: frontmatter.title ?? "",
78
+ description: frontmatter.description ?? "",
79
+ body: stripMdx(content),
80
+ group: entry.group,
81
+ section: entry.section,
82
+ roles: entry.roles,
83
+ })
84
+ }
85
+
86
+ fs.writeFileSync(OUT_FILE, JSON.stringify(index, null, 2))
87
+ console.log(`search-index.json written — ${index.length} docs`)
88
+ }
89
+
90
+ main().catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Assembles per-page PDFs into a single master PDF with:
3
+ * - Cover page (title, subtitle, version, date)
4
+ * - Table of contents with page numbers
5
+ * - All doc pages in nav order
6
+ * - PDF bookmarks (outline) matching nav structure
7
+ *
8
+ * Usage:
9
+ * npx tsx scripts/generate-master-pdf.ts [--version v2.0] [--pages .pdf-pages] [--out offline-builds]
10
+ */
11
+
12
+ import { PDFDocument, StandardFonts, rgb, PDFPage } from "pdf-lib"
13
+ import fs from "fs"
14
+ import path from "path"
15
+
16
+ const ROOT = path.resolve(process.cwd())
17
+
18
+ const VERSION = process.argv.find((a) => a.startsWith("--version="))?.split("=")[1] ?? "v2.0"
19
+ const PAGES_DIR = process.argv.find((a) => a.startsWith("--pages="))?.split("=")[1]
20
+ ? path.resolve(process.argv.find((a) => a.startsWith("--pages="))!.split("=")[1])
21
+ : path.join(ROOT, ".pdf-pages")
22
+ const OUT_DIR = process.argv.find((a) => a.startsWith("--out="))?.split("=")[1]
23
+ ? path.resolve(process.argv.find((a) => a.startsWith("--out="))!.split("=")[1])
24
+ : path.join(ROOT, "offline-builds")
25
+
26
+ type ManifestEntry = { slug: string; title: string; group: string; section?: string; file: string }
27
+
28
+ // ── Colors ──────────────────────────────────────────────────────────────────
29
+ const DARK = rgb(0.067, 0.094, 0.153) // #111827
30
+ const MID = rgb(0.42, 0.447, 0.502) // #6b7280
31
+ const LIGHT = rgb(0.97, 0.97, 0.97) // #f7f7f7
32
+ const WHITE = rgb(1, 1, 1)
33
+ const ACCENT = rgb(0.067, 0.094, 0.153) // same dark for simplicity
34
+
35
+ // Letter page dimensions (points)
36
+ const PW = 612
37
+ const PH = 792
38
+ const MARGIN = 64
39
+
40
+ // ── Cover page ───────────────────────────────────────────────────────────────
41
+ async function buildCoverPage(doc: PDFDocument): Promise<PDFPage> {
42
+ const page = doc.addPage([PW, PH])
43
+ const boldFont = await doc.embedFont(StandardFonts.HelveticaBold)
44
+ const regularFont = await doc.embedFont(StandardFonts.Helvetica)
45
+
46
+ // Dark background block (top 55% of page)
47
+ page.drawRectangle({ x: 0, y: PH * 0.45, width: PW, height: PH * 0.55, color: DARK })
48
+
49
+ // Thin accent line
50
+ page.drawRectangle({ x: MARGIN, y: PH * 0.45 - 2, width: PW - MARGIN * 2, height: 2, color: rgb(0.3, 0.35, 0.45) })
51
+
52
+ // "SECOND FRONT SYSTEMS" label
53
+ page.drawText("SECOND FRONT SYSTEMS", {
54
+ x: MARGIN,
55
+ y: PH * 0.45 + 180,
56
+ size: 9,
57
+ font: regularFont,
58
+ color: rgb(0.6, 0.65, 0.75),
59
+ characterSpacing: 2,
60
+ })
61
+
62
+ // Main title
63
+ page.drawText("Game Warden", {
64
+ x: MARGIN,
65
+ y: PH * 0.45 + 130,
66
+ size: 42,
67
+ font: boldFont,
68
+ color: WHITE,
69
+ })
70
+ page.drawText("Help Center", {
71
+ x: MARGIN,
72
+ y: PH * 0.45 + 78,
73
+ size: 42,
74
+ font: boldFont,
75
+ color: WHITE,
76
+ })
77
+
78
+ // Version + date below the dark block
79
+ const dateStr = new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
80
+ page.drawText(`Documentation — ${VERSION}`, {
81
+ x: MARGIN,
82
+ y: PH * 0.45 - 40,
83
+ size: 13,
84
+ font: boldFont,
85
+ color: DARK,
86
+ })
87
+ page.drawText(`Generated ${dateStr}`, {
88
+ x: MARGIN,
89
+ y: PH * 0.45 - 62,
90
+ size: 10,
91
+ font: regularFont,
92
+ color: MID,
93
+ })
94
+
95
+ // Footer bar
96
+ page.drawRectangle({ x: 0, y: 0, width: PW, height: 36, color: LIGHT })
97
+ page.drawText("game warden help center · secondfront.com", {
98
+ x: MARGIN,
99
+ y: 12,
100
+ size: 8,
101
+ font: regularFont,
102
+ color: MID,
103
+ characterSpacing: 0.5,
104
+ })
105
+ page.drawText("CONFIDENTIAL — FOR AUTHORIZED USERS ONLY", {
106
+ x: PW - MARGIN - 220,
107
+ y: 12,
108
+ size: 8,
109
+ font: regularFont,
110
+ color: MID,
111
+ })
112
+
113
+ return page
114
+ }
115
+
116
+ // ── TOC page ─────────────────────────────────────────────────────────────────
117
+ async function buildTocPage(
118
+ doc: PDFDocument,
119
+ entries: Array<{ title: string; group: string; section?: string; pageNum: number }>
120
+ ): Promise<void> {
121
+ const boldFont = await doc.embedFont(StandardFonts.HelveticaBold)
122
+ const regularFont = await doc.embedFont(StandardFonts.Helvetica)
123
+
124
+ let page = doc.addPage([PW, PH])
125
+ let y = PH - MARGIN
126
+
127
+ const drawPageHeader = (p: PDFPage) => {
128
+ p.drawText("TABLE OF CONTENTS", {
129
+ x: MARGIN, y: PH - MARGIN + 10, size: 8, font: regularFont, color: MID, characterSpacing: 1.5
130
+ })
131
+ p.drawLine({ start: { x: MARGIN, y: PH - MARGIN - 4 }, end: { x: PW - MARGIN, y: PH - MARGIN - 4 }, thickness: 0.5, color: rgb(0.85, 0.85, 0.85) })
132
+ }
133
+
134
+ drawPageHeader(page)
135
+ y -= 30
136
+
137
+ // Title
138
+ page.drawText("Contents", { x: MARGIN, y, size: 24, font: boldFont, color: DARK })
139
+ y -= 40
140
+
141
+ let lastGroup = ""
142
+
143
+ for (const entry of entries) {
144
+ // New page if needed
145
+ if (y < MARGIN + 30) {
146
+ page = doc.addPage([PW, PH])
147
+ drawPageHeader(page)
148
+ y = PH - MARGIN - 30
149
+ }
150
+
151
+ // Group header
152
+ if (entry.group !== lastGroup) {
153
+ if (lastGroup !== "") y -= 6
154
+ page.drawText(entry.group.toUpperCase(), {
155
+ x: MARGIN, y, size: 7.5, font: boldFont, color: MID, characterSpacing: 1,
156
+ })
157
+ y -= 18
158
+ lastGroup = entry.group
159
+ }
160
+
161
+ // Indent for section children
162
+ const indent = entry.section ? 16 : 0
163
+ const titleStr = entry.title
164
+ const pageStr = String(entry.pageNum)
165
+ const titleSize = entry.section ? 9 : 10
166
+ const titleFont = entry.section ? regularFont : boldFont
167
+ const titleColor = entry.section ? MID : DARK
168
+
169
+ // Dot leader
170
+ const titleWidth = titleFont.widthOfTextAtSize(titleStr, titleSize)
171
+ const pageWidth = regularFont.widthOfTextAtSize(pageStr, 9)
172
+ const leaderStart = MARGIN + indent + titleWidth + 4
173
+ const leaderEnd = PW - MARGIN - pageWidth - 8
174
+ if (leaderEnd > leaderStart) {
175
+ page.drawText("· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·", {
176
+ x: leaderStart, y: y + 1, size: 7, font: regularFont, color: rgb(0.8, 0.8, 0.8),
177
+ maxWidth: leaderEnd - leaderStart,
178
+ })
179
+ }
180
+
181
+ page.drawText(titleStr, { x: MARGIN + indent, y, size: titleSize, font: titleFont, color: titleColor })
182
+ page.drawText(pageStr, { x: PW - MARGIN - pageWidth, y, size: 9, font: regularFont, color: MID })
183
+
184
+ y -= entry.section ? 15 : 17
185
+ }
186
+ }
187
+
188
+ // ── Main ─────────────────────────────────────────────────────────────────────
189
+ async function main() {
190
+ const manifestPath = path.join(PAGES_DIR, "manifest.json")
191
+ if (!fs.existsSync(manifestPath)) {
192
+ console.error(`✗ manifest.json not found in ${PAGES_DIR}. Run generate-pdfs.ts first.`)
193
+ process.exit(1)
194
+ }
195
+
196
+ const manifest: ManifestEntry[] = JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
197
+ console.log(`▶ Assembling master PDF from ${manifest.length} pages (version ${VERSION})`)
198
+
199
+ // ── Pass 1: merge all content pages to count page numbers ──────────────────
200
+ const masterDoc = await PDFDocument.create()
201
+
202
+ // Reserve slots: cover (1 page) + TOC (we'll insert after knowing size)
203
+ // First collect all content pages so we can calculate TOC page numbers
204
+ const contentPages: Array<{ entry: ManifestEntry; startPage: number; pageCount: number }> = []
205
+
206
+ // Page offset: 1 cover + estimated TOC pages (we'll finalize after)
207
+ const TOC_PAGES = 2 // estimate; adjust if content overflows
208
+
209
+ let contentPageOffset = 1 + TOC_PAGES // cover + toc
210
+ const pageDocs: PDFDocument[] = []
211
+
212
+ for (const entry of manifest) {
213
+ if (!fs.existsSync(entry.file)) {
214
+ console.warn(` ⚠ Missing: ${entry.file}`)
215
+ continue
216
+ }
217
+ const bytes = fs.readFileSync(entry.file)
218
+ const srcDoc = await PDFDocument.load(bytes)
219
+ const count = srcDoc.getPageCount()
220
+ contentPages.push({ entry, startPage: contentPageOffset, pageCount: count })
221
+ pageDocs.push(srcDoc)
222
+ contentPageOffset += count
223
+ }
224
+
225
+ // ── Build cover ────────────────────────────────────────────────────────────
226
+ await buildCoverPage(masterDoc)
227
+
228
+ // ── Build TOC ──────────────────────────────────────────────────────────────
229
+ const tocEntries = contentPages.map((cp) => ({
230
+ title: cp.entry.title,
231
+ group: cp.entry.group,
232
+ section: cp.entry.section,
233
+ pageNum: cp.startPage,
234
+ }))
235
+ await buildTocPage(masterDoc, tocEntries)
236
+
237
+ // ── Copy content pages ─────────────────────────────────────────────────────
238
+ for (let i = 0; i < pageDocs.length; i++) {
239
+ const srcDoc = pageDocs[i]
240
+ const indices = srcDoc.getPageIndices()
241
+ const copied = await masterDoc.copyPages(srcDoc, indices)
242
+ copied.forEach((p) => masterDoc.addPage(p))
243
+ console.log(` ✓ ${manifest[i]?.title ?? "??"} (${srcDoc.getPageCount()}pp)`)
244
+ }
245
+
246
+ // ── Add bookmarks ──────────────────────────────────────────────────────────
247
+ // pdf-lib outline support is limited; we add top-level group markers
248
+ // (full nested outline requires low-level PDF manipulation — skipped for now)
249
+
250
+ // ── Write output ───────────────────────────────────────────────────────────
251
+ fs.mkdirSync(OUT_DIR, { recursive: true })
252
+ const outFile = path.join(OUT_DIR, `Game-Warden-Help-Center-${VERSION}.pdf`)
253
+ const pdfBytes = await masterDoc.save()
254
+ fs.writeFileSync(outFile, pdfBytes)
255
+
256
+ const sizeMb = (pdfBytes.length / 1024 / 1024).toFixed(1)
257
+ console.log(`\n✓ Master PDF: ${outFile} (${sizeMb} MB, ${masterDoc.getPageCount()} pages)`)
258
+ }
259
+
260
+ main().catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Generates one PDF per doc page using Playwright headless print.
3
+ * Pages are captured from the live Next.js dev server (port 3000).
4
+ *
5
+ * Usage:
6
+ * npx tsx scripts/generate-pdfs.ts [--port 3000] [--out .pdf-pages]
7
+ *
8
+ * Outputs: .pdf-pages/<slug-flattened>.pdf (temp dir, consumed by generate-master-pdf.ts)
9
+ */
10
+
11
+ import { chromium } from "playwright"
12
+ import { SignJWT } from "jose"
13
+ import fs from "fs"
14
+ import path from "path"
15
+ import yaml from "js-yaml"
16
+
17
+ const ROOT = path.resolve(process.cwd())
18
+ const NAV_FILE = path.join(ROOT, "nav", "nav.yml")
19
+
20
+ const PORT = parseInt(process.argv.find((a) => a.startsWith("--port="))?.split("=")[1] ?? "3000")
21
+ const OUT_DIR = process.argv.find((a) => a.startsWith("--out="))?.split("=")[1]
22
+ ? path.resolve(process.argv.find((a) => a.startsWith("--out="))!.split("=")[1])
23
+ : path.join(ROOT, ".pdf-pages")
24
+
25
+ const CONCURRENCY = 4
26
+ const BASE_URL = `http://localhost:${PORT}`
27
+
28
+ type NavChild = { label: string; slug: string; roles?: string[]; children?: NavChild[] }
29
+ type NavEntry = { label: string; slug: string; file?: string; roles?: string[]; section?: NavChild[] }
30
+ type NavGroup = { label: string; dropdown?: boolean; items?: NavEntry[] }
31
+
32
+ type PageEntry = { slug: string; title: string; group: string; section?: string }
33
+
34
+ function collectPages(nav: (NavEntry | NavGroup)[]): PageEntry[] {
35
+ const pages: PageEntry[] = []
36
+
37
+ for (const item of nav) {
38
+ if ("items" in item && item.items) {
39
+ for (const entry of item.items) {
40
+ if (entry.slug && entry.file) {
41
+ pages.push({ slug: entry.slug, title: entry.label, group: item.label })
42
+ }
43
+ for (const section of entry.section ?? []) {
44
+ if ((section as NavEntry).file) {
45
+ pages.push({ slug: section.slug, title: section.label, group: item.label, section: entry.label })
46
+ }
47
+ for (const child of section.children ?? []) {
48
+ if ((child as NavEntry).file) {
49
+ pages.push({ slug: child.slug, title: child.label, group: item.label, section: section.label })
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ return pages
58
+ }
59
+
60
+ function slugToFilename(slug: string): string {
61
+ return slug.replace(/^\//, "").replace(/\//g, "__") + ".pdf"
62
+ }
63
+
64
+ type Cookie = Awaited<ReturnType<import("playwright").BrowserContext["cookies"]>>[number]
65
+
66
+ async function generatePage(
67
+ browser: import("playwright").Browser,
68
+ page: PageEntry,
69
+ outDir: string,
70
+ cookies: Cookie[]
71
+ ): Promise<{ page: PageEntry; file: string } | null> {
72
+ const url = `${BASE_URL}${page.slug}`
73
+ const outFile = path.join(outDir, slugToFilename(page.slug))
74
+
75
+ const ctx = await browser.newContext()
76
+ await ctx.addCookies(cookies)
77
+ const pw = await ctx.newPage()
78
+
79
+ try {
80
+ await pw.goto(url, { waitUntil: "networkidle", timeout: 30000 })
81
+ await pw.waitForSelector("article", { timeout: 10000 })
82
+
83
+ // Force-open all <details> elements so their content is captured
84
+ // Also release height/overflow constraints that would clip content in print
85
+ await pw.evaluate(() => {
86
+ document.querySelectorAll("details").forEach((d) => { d.open = true })
87
+
88
+ // Show print-only elements by removing the Tailwind 'hidden' class
89
+ document.querySelectorAll<HTMLElement>('[data-print="show"]').forEach((el) => {
90
+ el.classList.remove("hidden")
91
+ })
92
+
93
+ // Release Tailwind h-screen / overflow-hidden / overflow-y-auto layout
94
+ const style = document.createElement("style")
95
+ style.textContent = `
96
+ html, body, .h-screen, .h-full { height: auto !important; min-height: 0 !important; }
97
+ .overflow-hidden, .overflow-y-auto, .overflow-x-hidden { overflow: visible !important; }
98
+ .flex-1 { flex: none !important; height: auto !important; }
99
+ nav, aside, [data-print="hide"] { display: none !important; }
100
+ `
101
+ document.head.appendChild(style)
102
+ })
103
+
104
+ await pw.pdf({
105
+ path: outFile,
106
+ format: "Letter",
107
+ printBackground: true,
108
+ margin: { top: "18mm", right: "16mm", bottom: "18mm", left: "16mm" },
109
+ displayHeaderFooter: true,
110
+ headerTemplate: `
111
+ <div style="width:100%;font-family:system-ui,sans-serif;font-size:8px;color:#6b7280;display:flex;justify-content:space-between;padding:0 16mm;">
112
+ <span>${page.group}${page.section ? " › " + page.section : ""}</span>
113
+ <span style="color:#111827;font-weight:600;">Game Warden Help Center</span>
114
+ </div>`,
115
+ footerTemplate: `
116
+ <div style="width:100%;font-family:system-ui,sans-serif;font-size:8px;color:#6b7280;display:flex;justify-content:space-between;padding:0 16mm;">
117
+ <span>${page.title}</span>
118
+ <span></span>
119
+ </div>`,
120
+ })
121
+
122
+ return { page, file: outFile }
123
+ } catch (e) {
124
+ console.warn(` ⚠ Failed: ${url} — ${(e as Error).message}`)
125
+ return null
126
+ } finally {
127
+ await ctx.close()
128
+ }
129
+ }
130
+
131
+ async function main() {
132
+ // Read nav
133
+ const raw = fs.readFileSync(NAV_FILE, "utf-8")
134
+ const nav = (yaml.load(raw) as { nav: (NavEntry | NavGroup)[] }).nav
135
+ const pages = collectPages(nav)
136
+
137
+ console.log(`▶ Generating PDFs for ${pages.length} pages → ${OUT_DIR}`)
138
+ fs.mkdirSync(OUT_DIR, { recursive: true })
139
+
140
+ // Check server is up
141
+ try {
142
+ const res = await fetch(`${BASE_URL}/home`)
143
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
144
+ } catch {
145
+ console.error(`✗ Dev server not reachable at ${BASE_URL}. Run: npm run dev`)
146
+ process.exit(1)
147
+ }
148
+
149
+ const browser = await chromium.launch()
150
+
151
+ // Sign in via the dev login UI so we get a real session cookie
152
+ const authCtx = await browser.newContext()
153
+ const authPage = await authCtx.newPage()
154
+ await authPage.goto(`${BASE_URL}/login`, { waitUntil: "networkidle" })
155
+ // Click "Staff (vendor + admin)" persona button
156
+ await authPage.click("button:has-text('Staff')")
157
+ await authPage.waitForURL(/\/home|\/getting-started/, { timeout: 10000 })
158
+ const cookies = await authCtx.cookies()
159
+ await authPage.close()
160
+ await authCtx.close()
161
+
162
+ const results: Array<{ page: PageEntry; file: string }> = []
163
+
164
+ // Process in batches of CONCURRENCY
165
+ for (let i = 0; i < pages.length; i += CONCURRENCY) {
166
+ const batch = pages.slice(i, i + CONCURRENCY)
167
+ const done = await Promise.all(batch.map((p) => generatePage(browser, p, OUT_DIR, cookies)))
168
+ for (const r of done) {
169
+ if (r) {
170
+ results.push(r)
171
+ console.log(` ✓ ${r.page.slug}`)
172
+ }
173
+ }
174
+ }
175
+
176
+ await browser.close()
177
+
178
+ // Write manifest — ordered list for master PDF assembly
179
+ const manifest = results.map((r) => ({ slug: r.page.slug, title: r.page.title, group: r.page.group, section: r.page.section, file: r.file }))
180
+ fs.writeFileSync(path.join(OUT_DIR, "manifest.json"), JSON.stringify(manifest, null, 2))
181
+
182
+ console.log(`✓ ${results.length}/${pages.length} pages generated`)
183
+ }
184
+
185
+ main().catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules", "scripts"]
34
+ }
@@ -0,0 +1,5 @@
1
+ versions:
2
+ - id: "latest"
3
+ label: "Latest"
4
+ stable: true
5
+ nav: nav/nav.yml