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.
- package/dist/index.js +142 -0
- package/package.json +51 -0
- package/template/_env.example +16 -0
- package/template/_gitignore +32 -0
- package/template/_package.json +39 -0
- package/template/app/[...slug]/page.tsx +150 -0
- package/template/app/api/auth/callback/route.ts +47 -0
- package/template/app/api/auth/login/route.ts +39 -0
- package/template/app/api/auth/logout/route.ts +8 -0
- package/template/app/api/auth/signin/route.ts +64 -0
- package/template/app/api/download/route.ts +67 -0
- package/template/app/api/raw/route.ts +43 -0
- package/template/app/api/search/route.ts +138 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +524 -0
- package/template/app/home/page.tsx +136 -0
- package/template/app/icon.svg +29 -0
- package/template/app/layout.tsx +44 -0
- package/template/app/login/LoginForm.tsx +105 -0
- package/template/app/login/page.tsx +9 -0
- package/template/app/page.tsx +5 -0
- package/template/camelmind.config.ts +33 -0
- package/template/components/Breadcrumbs/Breadcrumbs.tsx +41 -0
- package/template/components/Code/CodeBlock.tsx +55 -0
- package/template/components/DocActions/DocActions.tsx +62 -0
- package/template/components/Nav/MobileDrawer.tsx +221 -0
- package/template/components/Nav/ThemeToggle.tsx +62 -0
- package/template/components/Nav/TopNav.tsx +173 -0
- package/template/components/Nav/VersionSelector.tsx +107 -0
- package/template/components/PageNav/PageNav.tsx +68 -0
- package/template/components/Search/SearchModal.tsx +278 -0
- package/template/components/SectionCards/SectionCards.tsx +33 -0
- package/template/components/Sidebar/Sidebar.tsx +196 -0
- package/template/components/Toc/Toc.tsx +57 -0
- package/template/components/ZoomImages/ZoomImages.tsx +24 -0
- package/template/components/mdx/Callout.tsx +43 -0
- package/template/components/mdx/Details.tsx +65 -0
- package/template/components/mdx/Icon.tsx +25 -0
- package/template/components/mdx/Steps.tsx +33 -0
- package/template/components/mdx/Tabs.tsx +69 -0
- package/template/components/mdx/index.tsx +34 -0
- package/template/content/getting-started/installation.mdx +51 -0
- package/template/content/getting-started/overview.mdx +39 -0
- package/template/content/guides/writing-docs.mdx +72 -0
- package/template/eslint.config.mjs +18 -0
- package/template/lib/auth-providers/dev-mock.ts +45 -0
- package/template/lib/auth-providers/oidc.ts +95 -0
- package/template/lib/auth-roles.ts +28 -0
- package/template/lib/auth.ts +76 -0
- package/template/lib/config-types.ts +30 -0
- package/template/lib/config.ts +29 -0
- package/template/lib/mdx.ts +53 -0
- package/template/lib/nav-types.ts +31 -0
- package/template/lib/nav.ts +125 -0
- package/template/lib/versions.ts +61 -0
- package/template/nav/nav.yml +20 -0
- package/template/next.config.ts +35 -0
- package/template/postcss.config.mjs +7 -0
- package/template/proxy.ts +50 -0
- package/template/public/2f-logo.png +0 -0
- package/template/public/favicon.png +0 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/images/gwa-app-central-main.png +0 -0
- package/template/public/next.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/scripts/build-offline.sh +189 -0
- package/template/scripts/build-pdf.sh +41 -0
- package/template/scripts/build-search-index.ts +90 -0
- package/template/scripts/generate-master-pdf.ts +260 -0
- package/template/scripts/generate-pdfs.ts +185 -0
- package/template/tsconfig.json +34 -0
- 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
|
+
}
|