pdf-smith 0.1.0 → 0.2.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/server.d.ts CHANGED
@@ -3,7 +3,6 @@ interface PreviewOptions {
3
3
  root: string;
4
4
  /** Dev server port (default: 3000) */
5
5
  port?: number;
6
- pages?: string;
7
6
  /** Auto-open browser (default: false) */
8
7
  open?: boolean;
9
8
  }
@@ -15,15 +14,14 @@ interface PreviewServer {
15
14
 
16
15
  interface ExportOptions {
17
16
  root: string;
17
+ document?: string;
18
18
  output?: string;
19
- pageNumbers?: {
20
- enabled: boolean;
21
- template?: string;
22
- };
23
- pages?: string;
24
19
  }
25
20
  interface ExportResult {
26
- outputPath: string;
21
+ outputs: Array<{
22
+ document: string;
23
+ outputPath: string;
24
+ }>;
27
25
  }
28
26
 
29
27
  declare function exportPDF(options: ExportOptions): Promise<ExportResult>;
package/dist/server.js CHANGED
@@ -5,10 +5,39 @@ import { fileURLToPath } from "url";
5
5
  // src/export.ts
6
6
  import fs from "fs";
7
7
  import path from "path";
8
+ function discoverDocuments(root) {
9
+ const pdfsDir = path.join(root, "pdfs");
10
+ if (!fs.existsSync(pdfsDir)) return [];
11
+ const entries = fs.readdirSync(pdfsDir, { withFileTypes: true });
12
+ const documents = [];
13
+ for (const entry of entries) {
14
+ if (!entry.isDirectory()) continue;
15
+ const pagesDir = path.join(pdfsDir, entry.name, "pages");
16
+ if (!fs.existsSync(pagesDir)) continue;
17
+ const configPath = path.join(pdfsDir, entry.name, "config.ts");
18
+ documents.push({
19
+ slug: entry.name,
20
+ configPath: fs.existsSync(configPath) ? configPath : null
21
+ });
22
+ }
23
+ return documents;
24
+ }
8
25
  async function exportPDF(options) {
9
- const { root, output, pageNumbers, pages } = options;
10
- const outputPath = path.resolve(root, output ?? "./output.pdf");
11
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
26
+ const { root, document: documentSlug, output } = options;
27
+ const outputDir = path.resolve(root, output ?? "output");
28
+ let documents = discoverDocuments(root);
29
+ if (documentSlug) {
30
+ documents = documents.filter((d) => d.slug === documentSlug);
31
+ if (documents.length === 0) {
32
+ throw new Error(
33
+ `Document "${documentSlug}" not found. Check that pdfs/${documentSlug}/pages/ exists.`
34
+ );
35
+ }
36
+ }
37
+ if (documents.length === 0) {
38
+ throw new Error("No documents found. Create at least one document in pdfs/<name>/pages/.");
39
+ }
40
+ fs.mkdirSync(outputDir, { recursive: true });
12
41
  let playwright;
13
42
  try {
14
43
  playwright = await import("playwright");
@@ -18,42 +47,58 @@ async function exportPDF(options) {
18
47
  );
19
48
  }
20
49
  const { startPreview: startPreview2 } = await import("./server.js");
21
- const server = await startPreview2({ root, port: 0, pages, open: false });
50
+ const server = await startPreview2({ root, port: 0, open: false });
51
+ const vite = server._vite;
22
52
  const browser = await playwright.chromium.launch({ headless: true });
53
+ const outputs = [];
23
54
  try {
24
- const page = await browser.newPage();
25
- await page.goto(server.url, { waitUntil: "networkidle" });
26
- await page.waitForSelector("[data-pdf-smith-page]", { timeout: 3e4 });
27
- const margin = { top: "0", bottom: "0", left: "0", right: "0" };
28
- const pdfOptions = {
29
- path: outputPath,
30
- preferCSSPageSize: true,
31
- printBackground: true,
32
- margin
33
- };
34
- if (pageNumbers?.enabled) {
35
- pdfOptions.displayHeaderFooter = true;
36
- pdfOptions.headerTemplate = "<span></span>";
37
- pdfOptions.footerTemplate = pageNumbers.template ?? '<div style="font-size:10px;text-align:center;width:100%;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>';
38
- margin.bottom = "40px";
55
+ for (const doc of documents) {
56
+ let config;
57
+ if (doc.configPath && vite) {
58
+ try {
59
+ const configModule = await vite.ssrLoadModule(doc.configPath);
60
+ config = configModule.default;
61
+ } catch {
62
+ }
63
+ }
64
+ const filename = config?.output ?? `${doc.slug}.pdf`;
65
+ const outputPath = path.resolve(outputDir, filename);
66
+ const page = await browser.newPage();
67
+ await page.goto(`${server.url}/${doc.slug}`, { waitUntil: "networkidle" });
68
+ await page.waitForSelector("[data-pdf-smith-page]", { timeout: 3e4 });
69
+ const margin = { top: "0", bottom: "0", left: "0", right: "0" };
70
+ const pdfOptions = {
71
+ path: outputPath,
72
+ preferCSSPageSize: true,
73
+ printBackground: true,
74
+ margin
75
+ };
76
+ if (config?.pageNumbers?.enabled) {
77
+ pdfOptions.displayHeaderFooter = true;
78
+ pdfOptions.headerTemplate = "<span></span>";
79
+ pdfOptions.footerTemplate = config.pageNumbers.template ?? '<div style="font-size:10px;text-align:center;width:100%;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>';
80
+ margin.bottom = "40px";
81
+ }
82
+ await page.pdf(pdfOptions);
83
+ await page.close();
84
+ outputs.push({ document: doc.slug, outputPath });
39
85
  }
40
- await page.pdf(pdfOptions);
41
86
  } finally {
42
87
  await browser.close().catch(() => {
43
88
  });
44
89
  await server.close().catch(() => {
45
90
  });
46
91
  }
47
- return { outputPath };
92
+ return { outputs };
48
93
  }
49
94
 
50
95
  // src/server.ts
51
96
  async function startPreview(options) {
52
- const { root, port = 3e3, pages = "/pages/**/*.tsx", open = false } = options;
97
+ const { root, port = 3e3, open = false } = options;
53
98
  const { createServer } = await import("vite");
54
99
  const react = (await import("@vitejs/plugin-react")).default;
55
100
  const tailwindcss = (await import("@tailwindcss/vite")).default;
56
- const { pdfSmithPreviewPlugin } = await import("./preview-plugin-QWJEXDW2.js");
101
+ const { pdfSmithPreviewPlugin } = await import("./preview-plugin-QDLEMAEE.js");
57
102
  const currentDir = path2.dirname(fileURLToPath(import.meta.url));
58
103
  const pkgRoot = path2.resolve(currentDir, "..");
59
104
  const pkgSrcDir = path2.resolve(pkgRoot, "src");
@@ -75,7 +120,7 @@ async function startPreview(options) {
75
120
  },
76
121
  dedupe: ["react", "react-dom"]
77
122
  },
78
- plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir, pagesGlob: pages })],
123
+ plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir })],
79
124
  optimizeDeps: {
80
125
  include: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"]
81
126
  }
@@ -87,7 +132,7 @@ async function startPreview(options) {
87
132
  const address = server.httpServer?.address();
88
133
  const resolvedPort = address?.port ?? port;
89
134
  const url = `http://localhost:${resolvedPort}`;
90
- return {
135
+ const previewServer = {
91
136
  close: async () => {
92
137
  const httpServer = server.httpServer;
93
138
  httpServer?.closeAllConnections?.();
@@ -96,6 +141,11 @@ async function startPreview(options) {
96
141
  port: resolvedPort,
97
142
  url
98
143
  };
144
+ Object.defineProperty(previewServer, "_vite", {
145
+ value: server,
146
+ enumerable: false
147
+ });
148
+ return previewServer;
99
149
  }
100
150
  export {
101
151
  exportPDF,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/export.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport { exportPDF } from './export';\nexport type { ExportOptions, ExportResult } from './export-types';\nexport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport async function startPreview(options: PreviewOptions): Promise<PreviewServer> {\n const { root, port = 3000, pages = '/pages/**/*.tsx', open = false } = options;\n\n const { createServer } = await import('vite');\n const react = (await import('@vitejs/plugin-react')).default;\n const tailwindcss = (await import('@tailwindcss/vite')).default;\n const { pdfSmithPreviewPlugin } = await import('./preview/preview-plugin');\n\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.resolve(currentDir, '..');\n const pkgSrcDir = path.resolve(pkgRoot, 'src');\n\n const server = await createServer({\n configFile: false,\n appType: 'custom',\n root,\n server: {\n port,\n strictPort: port !== 0,\n open,\n fs: {\n allow: [root, pkgSrcDir, pkgRoot],\n },\n },\n resolve: {\n alias: {\n 'pdf-smith': path.resolve(pkgSrcDir, 'index.ts'),\n },\n dedupe: ['react', 'react-dom'],\n },\n plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir, pagesGlob: pages })],\n optimizeDeps: {\n include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],\n },\n });\n\n await server.listen();\n\n if (port !== 0) {\n server.printUrls();\n }\n\n const address = server.httpServer?.address() as AddressInfo | null;\n const resolvedPort = address?.port ?? port;\n const url = `http://localhost:${resolvedPort}`;\n\n return {\n close: async () => {\n const httpServer = server.httpServer as { closeAllConnections?: () => void } | null;\n httpServer?.closeAllConnections?.();\n await server.close();\n },\n port: resolvedPort,\n url,\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { ExportOptions, ExportResult } from './export-types';\n\nexport async function exportPDF(options: ExportOptions): Promise<ExportResult> {\n const { root, output, pageNumbers, pages } = options;\n const outputPath = path.resolve(root, output ?? './output.pdf');\n\n fs.mkdirSync(path.dirname(outputPath), { recursive: true });\n\n let playwright: typeof import('playwright');\n try {\n playwright = await import('playwright');\n } catch {\n throw new Error(\n 'Playwright is required for PDF export. Install it with: npm install -D playwright && npx playwright install chromium',\n );\n }\n\n const { startPreview } = await import('./server');\n const server = await startPreview({ root, port: 0, pages, open: false });\n\n const browser = await playwright.chromium.launch({ headless: true });\n try {\n const page = await browser.newPage();\n await page.goto(server.url, { waitUntil: 'networkidle' });\n await page.waitForSelector('[data-pdf-smith-page]', { timeout: 30_000 });\n\n const margin = { top: '0', bottom: '0', left: '0', right: '0' };\n\n const pdfOptions: Parameters<typeof page.pdf>[0] = {\n path: outputPath,\n preferCSSPageSize: true,\n printBackground: true,\n margin,\n };\n\n if (pageNumbers?.enabled) {\n pdfOptions.displayHeaderFooter = true;\n pdfOptions.headerTemplate = '<span></span>';\n pdfOptions.footerTemplate =\n pageNumbers.template ??\n '<div style=\"font-size:10px;text-align:center;width:100%;\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>';\n margin.bottom = '40px';\n }\n\n await page.pdf(pdfOptions);\n } finally {\n await browser.close().catch(() => {});\n await server.close().catch(() => {});\n }\n\n return { outputPath };\n}\n"],"mappings":"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","names":["path","startPreview","path"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/export.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport { exportPDF } from './export';\nexport type { ExportOptions, ExportResult } from './export-types';\nexport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport async function startPreview(options: PreviewOptions): Promise<PreviewServer> {\n const { root, port = 3000, open = false } = options;\n\n const { createServer } = await import('vite');\n const react = (await import('@vitejs/plugin-react')).default;\n const tailwindcss = (await import('@tailwindcss/vite')).default;\n const { pdfSmithPreviewPlugin } = await import('./preview/preview-plugin');\n\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.resolve(currentDir, '..');\n const pkgSrcDir = path.resolve(pkgRoot, 'src');\n\n const server = await createServer({\n configFile: false,\n appType: 'custom',\n root,\n server: {\n port,\n strictPort: port !== 0,\n open,\n fs: {\n allow: [root, pkgSrcDir, pkgRoot],\n },\n },\n resolve: {\n alias: {\n 'pdf-smith': path.resolve(pkgSrcDir, 'index.ts'),\n },\n dedupe: ['react', 'react-dom'],\n },\n plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir })],\n optimizeDeps: {\n include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],\n },\n });\n\n await server.listen();\n\n if (port !== 0) {\n server.printUrls();\n }\n\n const address = server.httpServer?.address() as AddressInfo | null;\n const resolvedPort = address?.port ?? port;\n const url = `http://localhost:${resolvedPort}`;\n\n const previewServer: PreviewServer = {\n close: async () => {\n const httpServer = server.httpServer as { closeAllConnections?: () => void } | null;\n httpServer?.closeAllConnections?.();\n await server.close();\n },\n port: resolvedPort,\n url,\n };\n\n Object.defineProperty(previewServer, '_vite', {\n value: server,\n enumerable: false,\n });\n\n return previewServer;\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { DocumentConfig } from './config';\nimport type { ExportOptions, ExportResult } from './export-types';\n\ninterface DiscoveredDocument {\n slug: string;\n configPath: string | null;\n}\n\nfunction discoverDocuments(root: string): DiscoveredDocument[] {\n const pdfsDir = path.join(root, 'pdfs');\n if (!fs.existsSync(pdfsDir)) return [];\n\n const entries = fs.readdirSync(pdfsDir, { withFileTypes: true });\n const documents: DiscoveredDocument[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const pagesDir = path.join(pdfsDir, entry.name, 'pages');\n if (!fs.existsSync(pagesDir)) continue;\n\n const configPath = path.join(pdfsDir, entry.name, 'config.ts');\n documents.push({\n slug: entry.name,\n configPath: fs.existsSync(configPath) ? configPath : null,\n });\n }\n\n return documents;\n}\n\nexport async function exportPDF(options: ExportOptions): Promise<ExportResult> {\n const { root, document: documentSlug, output } = options;\n const outputDir = path.resolve(root, output ?? 'output');\n\n let documents = discoverDocuments(root);\n\n if (documentSlug) {\n documents = documents.filter((d) => d.slug === documentSlug);\n if (documents.length === 0) {\n throw new Error(\n `Document \"${documentSlug}\" not found. Check that pdfs/${documentSlug}/pages/ exists.`,\n );\n }\n }\n\n if (documents.length === 0) {\n throw new Error('No documents found. Create at least one document in pdfs/<name>/pages/.');\n }\n\n fs.mkdirSync(outputDir, { recursive: true });\n\n let playwright: typeof import('playwright');\n try {\n playwright = await import('playwright');\n } catch {\n throw new Error(\n 'Playwright is required for PDF export. Install it with: npm install -D playwright && npx playwright install chromium',\n );\n }\n\n const { startPreview } = await import('./server');\n const server = await startPreview({ root, port: 0, open: false });\n\n const vite = (\n server as unknown as {\n _vite: { ssrLoadModule: (id: string) => Promise<{ default?: DocumentConfig }> };\n }\n )._vite;\n\n const browser = await playwright.chromium.launch({ headless: true });\n const outputs: ExportResult['outputs'] = [];\n\n try {\n for (const doc of documents) {\n let config: DocumentConfig | undefined;\n if (doc.configPath && vite) {\n try {\n const configModule = await vite.ssrLoadModule(doc.configPath);\n config = configModule.default;\n } catch {\n // Config loading failed, proceed without it\n }\n }\n\n const filename = config?.output ?? `${doc.slug}.pdf`;\n const outputPath = path.resolve(outputDir, filename);\n\n const page = await browser.newPage();\n await page.goto(`${server.url}/${doc.slug}`, { waitUntil: 'networkidle' });\n await page.waitForSelector('[data-pdf-smith-page]', { timeout: 30_000 });\n\n const margin = { top: '0', bottom: '0', left: '0', right: '0' };\n\n const pdfOptions: Parameters<typeof page.pdf>[0] = {\n path: outputPath,\n preferCSSPageSize: true,\n printBackground: true,\n margin,\n };\n\n if (config?.pageNumbers?.enabled) {\n pdfOptions.displayHeaderFooter = true;\n pdfOptions.headerTemplate = '<span></span>';\n pdfOptions.footerTemplate =\n config.pageNumbers.template ??\n '<div style=\"font-size:10px;text-align:center;width:100%;\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>';\n margin.bottom = '40px';\n }\n\n await page.pdf(pdfOptions);\n await page.close();\n\n outputs.push({ document: doc.slug, outputPath });\n }\n } finally {\n await browser.close().catch(() => {});\n await server.close().catch(() => {});\n }\n\n return { outputs };\n}\n"],"mappings":"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","names":["path","startPreview","path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-smith",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Build beautiful PDFs with React components and Tailwind CSS",
5
5
  "author": "Kareem Elbahrawy",
6
6
  "license": "MIT",
@@ -20,6 +20,9 @@
20
20
  "react-pdf"
21
21
  ],
22
22
  "type": "module",
23
+ "bin": {
24
+ "pdf-smith": "./dist/cli.js"
25
+ },
23
26
  "main": "./dist/index.cjs",
24
27
  "module": "./dist/index.js",
25
28
  "types": "./dist/index.d.ts",
@@ -47,13 +50,17 @@
47
50
  },
48
51
  "files": [
49
52
  "dist",
53
+ "README.md",
50
54
  "src/preview/",
51
55
  "src/index.ts",
52
56
  "src/page.tsx",
53
57
  "src/document.tsx",
54
58
  "src/types.ts",
55
59
  "src/constants.ts",
56
- "src/styles.ts"
60
+ "src/styles.ts",
61
+ "src/config.ts",
62
+ "src/add.ts",
63
+ "src/cli.ts"
57
64
  ],
58
65
  "dependencies": {
59
66
  "vite": "^7.0.0",
package/src/add.ts ADDED
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const SLUG_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
5
+
6
+ export interface AddDocumentOptions {
7
+ slug: string;
8
+ root: string;
9
+ }
10
+
11
+ export interface AddDocumentResult {
12
+ paths: string[];
13
+ }
14
+
15
+ function toTitle(slug: string): string {
16
+ return slug
17
+ .split('-')
18
+ .map((w) => w[0].toUpperCase() + w.slice(1))
19
+ .join(' ');
20
+ }
21
+
22
+ export function addDocument({ slug, root }: AddDocumentOptions): AddDocumentResult {
23
+ if (!SLUG_REGEX.test(slug)) {
24
+ throw new Error(
25
+ `Invalid slug "${slug}". Use lowercase letters, numbers, and hyphens (e.g. "my-report").`,
26
+ );
27
+ }
28
+
29
+ const docDir = path.join(root, 'pdfs', slug);
30
+
31
+ if (fs.existsSync(docDir)) {
32
+ throw new Error(`Document "${slug}" already exists at ${docDir}`);
33
+ }
34
+
35
+ const pagesDir = path.join(docDir, 'pages');
36
+ fs.mkdirSync(pagesDir, { recursive: true });
37
+
38
+ const title = toTitle(slug);
39
+
40
+ const page1Content = `import '../../../styles.css';
41
+
42
+ export default function Page1() {
43
+ return (
44
+ <div className="h-full flex flex-col items-center justify-center p-12 font-sans">
45
+ <h1 className="text-4xl font-bold tracking-tight text-gray-900">${title}</h1>
46
+ <p className="mt-4 text-gray-500">Start editing this page to build your document.</p>
47
+ </div>
48
+ );
49
+ }
50
+ `;
51
+
52
+ const configContent = `import { defineConfig } from 'pdf-smith';
53
+
54
+ export default defineConfig({
55
+ pageNumbers: { enabled: false },
56
+ });
57
+ `;
58
+
59
+ const page1Path = path.join(pagesDir, 'Page1.tsx');
60
+ const configPath = path.join(docDir, 'config.ts');
61
+
62
+ fs.writeFileSync(page1Path, page1Content);
63
+ fs.writeFileSync(configPath, configContent);
64
+
65
+ return {
66
+ paths: [
67
+ path.relative(root, pagesDir),
68
+ path.relative(root, page1Path),
69
+ path.relative(root, configPath),
70
+ ],
71
+ };
72
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { addDocument } from './add';
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+
6
+ if (command === 'add') {
7
+ const slug = args[1];
8
+
9
+ if (!slug || slug === '--help') {
10
+ console.log('Usage: pdf-smith add <slug>');
11
+ process.exit(slug ? 0 : 1);
12
+ }
13
+
14
+ try {
15
+ addDocument({ slug, root: process.cwd() });
16
+ console.log(`Created document '${slug}'. Start editing at pdfs/${slug}/pages/Page1.tsx`);
17
+ } catch (error) {
18
+ console.error((error as Error).message);
19
+ process.exit(1);
20
+ }
21
+ } else {
22
+ console.log('Usage: pdf-smith add <slug>');
23
+ process.exit(command === '--help' || command === '-h' ? 0 : 1);
24
+ }
package/src/config.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface DocumentConfig {
2
+ pageNumbers?: { enabled: boolean; template?: string };
3
+ output?: string;
4
+ }
5
+
6
+ export function defineConfig(config: DocumentConfig): DocumentConfig {
7
+ return config;
8
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { DocumentConfig } from './config';
2
+ export { defineConfig } from './config';
1
3
  export { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, PAGE_SIZES } from './constants';
2
4
  export { Document } from './document';
3
5
  export { Page } from './page';
@@ -1,4 +1,4 @@
1
- import { getPages } from 'virtual:pdf-smith-pages';
1
+ import { getDocuments } from 'virtual:pdf-smith-documents';
2
2
  import { useState } from 'react';
3
3
  import { Document } from '../document';
4
4
  import { Page } from '../page';
@@ -26,15 +26,26 @@ const pageLabelStyle: React.CSSProperties = {
26
26
  width: 'fit-content',
27
27
  };
28
28
 
29
- export function PreviewApp() {
30
- const pages = getPages();
29
+ interface PreviewAppProps {
30
+ slug: string;
31
+ }
32
+
33
+ export function PreviewApp({ slug }: PreviewAppProps) {
34
+ const documents = getDocuments();
35
+ const doc = documents[slug];
36
+ const pages = doc?.pages ?? {};
31
37
  const [activePage, setActivePage] = useState<string | null>(null);
32
38
 
33
39
  const visiblePages = activePage ? { [activePage]: pages[activePage] } : pages;
34
40
 
35
41
  return (
36
42
  <>
37
- <Navigation pages={pages} activePage={activePage} onSelectPage={setActivePage} />
43
+ <Navigation
44
+ pages={pages}
45
+ activePage={activePage}
46
+ onSelectPage={setActivePage}
47
+ documentSlug={slug}
48
+ />
38
49
  <div data-pdf-smith-container="" style={containerStyle}>
39
50
  <Document>
40
51
  {Object.entries(visiblePages).map(([name, PageComponent]) => (
@@ -1,7 +1,11 @@
1
1
  import { createRoot } from 'react-dom/client';
2
2
  import { PreviewApp } from './app';
3
+ import { HomePage } from './home';
3
4
 
4
- const root = document.getElementById('root');
5
- if (root) {
6
- createRoot(root).render(<PreviewApp />);
5
+ const rootEl = document.getElementById('root');
6
+ if (rootEl) {
7
+ const path = window.location.pathname;
8
+ const slug = path === '/' ? null : path.replace(/^\//, '').replace(/\/$/, '');
9
+
10
+ createRoot(rootEl).render(slug ? <PreviewApp slug={slug} /> : <HomePage />);
7
11
  }
@@ -0,0 +1,95 @@
1
+ import { getDocuments } from 'virtual:pdf-smith-documents';
2
+
3
+ const containerStyle: React.CSSProperties = {
4
+ minHeight: '100vh',
5
+ background: '#1a1a2e',
6
+ color: '#e0e0e0',
7
+ fontFamily: 'system-ui, -apple-system, sans-serif',
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ alignItems: 'center',
11
+ padding: '48px 24px',
12
+ };
13
+
14
+ const titleStyle: React.CSSProperties = {
15
+ fontSize: '28px',
16
+ fontWeight: 700,
17
+ color: '#ffffff',
18
+ marginBottom: '8px',
19
+ letterSpacing: '0.5px',
20
+ };
21
+
22
+ const subtitleStyle: React.CSSProperties = {
23
+ fontSize: '14px',
24
+ color: '#888',
25
+ marginBottom: '40px',
26
+ };
27
+
28
+ const gridStyle: React.CSSProperties = {
29
+ display: 'grid',
30
+ gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
31
+ gap: '16px',
32
+ width: '100%',
33
+ maxWidth: '800px',
34
+ };
35
+
36
+ const cardStyle: React.CSSProperties = {
37
+ display: 'block',
38
+ background: '#16213e',
39
+ borderRadius: '8px',
40
+ padding: '24px',
41
+ textDecoration: 'none',
42
+ color: '#e0e0e0',
43
+ transition: 'background 0.15s',
44
+ border: '1px solid #2a2a4a',
45
+ };
46
+
47
+ const cardTitleStyle: React.CSSProperties = {
48
+ fontSize: '18px',
49
+ fontWeight: 600,
50
+ color: '#ffffff',
51
+ marginBottom: '8px',
52
+ };
53
+
54
+ const cardMetaStyle: React.CSSProperties = {
55
+ fontSize: '13px',
56
+ color: '#888',
57
+ };
58
+
59
+ export function HomePage() {
60
+ const documents = getDocuments();
61
+ const slugs = Object.keys(documents);
62
+
63
+ return (
64
+ <div style={containerStyle}>
65
+ <h1 style={titleStyle}>pdf-smith</h1>
66
+ <p style={subtitleStyle}>
67
+ {slugs.length} document{slugs.length !== 1 ? 's' : ''}
68
+ </p>
69
+ <div style={gridStyle}>
70
+ {slugs.map((slug) => {
71
+ const doc = documents[slug];
72
+ const pageCount = Object.keys(doc.pages).length;
73
+ return (
74
+ <a
75
+ key={slug}
76
+ href={`/${slug}`}
77
+ style={cardStyle}
78
+ onMouseEnter={(e) => {
79
+ e.currentTarget.style.background = '#1e2d50';
80
+ }}
81
+ onMouseLeave={(e) => {
82
+ e.currentTarget.style.background = '#16213e';
83
+ }}
84
+ >
85
+ <div style={cardTitleStyle}>{slug}</div>
86
+ <div style={cardMetaStyle}>
87
+ {pageCount} page{pageCount !== 1 ? 's' : ''}
88
+ </div>
89
+ </a>
90
+ );
91
+ })}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -2,6 +2,7 @@ interface NavigationProps {
2
2
  pages: Record<string, React.ComponentType>;
3
3
  activePage: string | null;
4
4
  onSelectPage: (page: string | null) => void;
5
+ documentSlug: string;
5
6
  }
6
7
 
7
8
  const navStyle: React.CSSProperties = {
@@ -28,6 +29,15 @@ const titleStyle: React.CSSProperties = {
28
29
  letterSpacing: '0.5px',
29
30
  };
30
31
 
32
+ const backLinkStyle: React.CSSProperties = {
33
+ display: 'block',
34
+ fontSize: '13px',
35
+ color: '#888',
36
+ textDecoration: 'none',
37
+ marginBottom: '12px',
38
+ transition: 'color 0.15s',
39
+ };
40
+
31
41
  const buttonBaseStyle: React.CSSProperties = {
32
42
  display: 'block',
33
43
  width: '100%',
@@ -57,12 +67,24 @@ const separatorStyle: React.CSSProperties = {
57
67
  margin: '12px 0',
58
68
  };
59
69
 
60
- export function Navigation({ pages, activePage, onSelectPage }: NavigationProps) {
70
+ export function Navigation({ pages, activePage, onSelectPage, documentSlug }: NavigationProps) {
61
71
  const pageNames = Object.keys(pages);
62
72
 
63
73
  return (
64
74
  <nav data-pdf-smith-nav="" style={navStyle}>
65
- <div style={titleStyle}>pdf-smith Preview</div>
75
+ <a
76
+ href="/"
77
+ style={backLinkStyle}
78
+ onMouseEnter={(e) => {
79
+ e.currentTarget.style.color = '#e0e0e0';
80
+ }}
81
+ onMouseLeave={(e) => {
82
+ e.currentTarget.style.color = '#888';
83
+ }}
84
+ >
85
+ &larr; All Documents
86
+ </a>
87
+ <div style={titleStyle}>{documentSlug}</div>
66
88
  <button
67
89
  type="button"
68
90
  style={getButtonStyle(activePage === null)}
@@ -1,21 +1,23 @@
1
1
  import type { Plugin } from 'vite';
2
2
 
3
- const VIRTUAL_PAGES_ID = 'virtual:pdf-smith-pages';
4
- const RESOLVED_VIRTUAL_PAGES_ID = `\0${VIRTUAL_PAGES_ID}`;
3
+ const VIRTUAL_DOCUMENTS_ID = 'virtual:pdf-smith-documents';
4
+ const RESOLVED_VIRTUAL_DOCUMENTS_ID = `\0${VIRTUAL_DOCUMENTS_ID}`;
5
5
 
6
6
  interface PreviewPluginOptions {
7
7
  pkgSrcDir: string;
8
- pagesGlob: string;
9
8
  }
10
9
 
11
- export function pdfSmithPreviewPlugin({ pkgSrcDir, pagesGlob }: PreviewPluginOptions): Plugin {
10
+ export function pdfSmithPreviewPlugin({ pkgSrcDir }: PreviewPluginOptions): Plugin {
12
11
  return {
13
12
  name: 'pdf-smith-preview',
14
13
 
15
14
  configureServer(server) {
16
15
  return () => {
17
16
  server.middlewares.use((req, res, next) => {
18
- if (req.url !== '/' && req.url !== '/index.html') {
17
+ const url = req.url ?? '';
18
+
19
+ // Skip file requests, Vite internals, and node_modules
20
+ if (url.includes('.') || url.startsWith('/@') || url.startsWith('/node_modules/')) {
19
21
  next();
20
22
  return;
21
23
  }
@@ -65,25 +67,44 @@ window.__vite_plugin_react_preamble_installed__ = true
65
67
  },
66
68
 
67
69
  resolveId(id) {
68
- if (id === VIRTUAL_PAGES_ID) {
69
- return RESOLVED_VIRTUAL_PAGES_ID;
70
+ if (id === VIRTUAL_DOCUMENTS_ID) {
71
+ return RESOLVED_VIRTUAL_DOCUMENTS_ID;
70
72
  }
71
73
  },
72
74
 
73
75
  load(id) {
74
- if (id === RESOLVED_VIRTUAL_PAGES_ID) {
76
+ if (id === RESOLVED_VIRTUAL_DOCUMENTS_ID) {
75
77
  return `
76
- const modules = import.meta.glob('${pagesGlob}', { eager: true });
78
+ const pageModules = import.meta.glob('/pdfs/*/pages/**/*.tsx', { eager: true });
79
+ const configModules = import.meta.glob('/pdfs/*/config.ts', { eager: true });
80
+
81
+ export function getDocuments() {
82
+ const documents = {};
77
83
 
78
- export function getPages() {
79
- const pages = {};
80
- for (const [path, mod] of Object.entries(modules)) {
81
- const name = path
82
- .replace(/^\\/pages\\//, '')
83
- .replace(/\\.[^.]+$/, '');
84
- pages[name] = mod.default;
84
+ for (const [path, mod] of Object.entries(pageModules)) {
85
+ const match = path.match(/^\\/pdfs\\/([^/]+)\\/pages\\/(.+)\\.[^.]+$/);
86
+ if (!match) continue;
87
+ const [, slug, pageName] = match;
88
+ if (!documents[slug]) {
89
+ documents[slug] = { slug, pages: {}, config: undefined };
90
+ }
91
+ documents[slug].pages[pageName] = mod.default;
85
92
  }
86
- return pages;
93
+
94
+ for (const [path, mod] of Object.entries(configModules)) {
95
+ const match = path.match(/^\\/pdfs\\/([^/]+)\\/config\\.ts$/);
96
+ if (!match) continue;
97
+ const slug = match[1];
98
+ if (documents[slug]) {
99
+ documents[slug].config = mod.default;
100
+ }
101
+ }
102
+
103
+ return documents;
104
+ }
105
+
106
+ export function getDocumentSlugs() {
107
+ return Object.keys(getDocuments());
87
108
  }
88
109
  `;
89
110
  }
@@ -3,8 +3,6 @@ export interface PreviewOptions {
3
3
  root: string;
4
4
  /** Dev server port (default: 3000) */
5
5
  port?: number;
6
- // Glob pattern for page discovery (default: '/pages/**/*.tsx')
7
- pages?: string;
8
6
  /** Auto-open browser (default: false) */
9
7
  open?: boolean;
10
8
  }