doxla 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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/cli/index.js +2 -4
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/app/src/App.tsx +30 -2
- package/src/app/src/components/DocPage.tsx +4 -2
- package/src/app/src/components/MarkdownRenderer.tsx +37 -29
- package/src/app/src/components/layout/Header.tsx +14 -2
- package/src/app/src/components/layout/Layout.tsx +5 -2
- package/src/app/src/globals.css +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Doxla
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# doxla
|
|
2
|
+
|
|
3
|
+
Improve documentation discoverability within repos. Doxla discovers all `.md` files in your repository, builds a beautiful docs viewer, and deploys it to GitHub Pages.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Deploy to GitHub Pages
|
|
8
|
+
|
|
9
|
+
Run this in your repo to set up automatic deployment:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx doxla init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This creates a GitHub Actions workflow that builds and deploys your docs on every push to `main`.
|
|
16
|
+
|
|
17
|
+
**Important:** You must enable GitHub Pages in your repo settings before the workflow will deploy:
|
|
18
|
+
|
|
19
|
+
1. Go to **Settings** > **Pages**
|
|
20
|
+
2. Under **Source**, select **GitHub Actions**
|
|
21
|
+
3. Commit and push — the workflow handles the rest
|
|
22
|
+
|
|
23
|
+
### Build locally
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx doxla build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This discovers all markdown files, builds a static docs site, and outputs it to `doxla-dist/`.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Automatic markdown file discovery (respects `.gitignore`-style patterns)
|
|
34
|
+
- Beautiful React-based docs viewer with sidebar navigation
|
|
35
|
+
- Full-text search across all documents
|
|
36
|
+
- Syntax highlighting for code blocks
|
|
37
|
+
- GitHub Flavored Markdown (tables, task lists, strikethrough)
|
|
38
|
+
- Responsive layout
|
|
39
|
+
- Zero configuration required
|
|
40
|
+
|
|
41
|
+
## CLI Options
|
|
42
|
+
|
|
43
|
+
### `doxla init`
|
|
44
|
+
|
|
45
|
+
Creates `.github/workflows/doxla.yml` for automatic GitHub Pages deployment.
|
|
46
|
+
|
|
47
|
+
### `doxla build`
|
|
48
|
+
|
|
49
|
+
| Option | Default | Description |
|
|
50
|
+
|--------|---------|-------------|
|
|
51
|
+
| `-o, --output <dir>` | `doxla-dist` | Output directory |
|
|
52
|
+
| `-r, --root <dir>` | `.` | Root directory to scan for markdown |
|
|
53
|
+
| `--base-path <path>` | `/` | Base path for GitHub Pages (e.g. `/my-repo`) |
|
|
54
|
+
|
|
55
|
+
## How It Works
|
|
56
|
+
|
|
57
|
+
1. **Discover** - Scans your repo for `.md` files (excluding `node_modules`, `.git`, etc.)
|
|
58
|
+
2. **Manifest** - Reads each file, extracts titles, and generates a JSON manifest
|
|
59
|
+
3. **Build** - Copies the built-in React app template, injects the manifest, and runs `vite build`
|
|
60
|
+
4. **Output** - Produces a static site ready for any hosting
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm install
|
|
66
|
+
pnpm build # Build the CLI
|
|
67
|
+
pnpm test # Run tests
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/cli/index.js
CHANGED
|
@@ -180,16 +180,14 @@ async function buildApp(manifest, options) {
|
|
|
180
180
|
JSON.stringify(manifest, null, 2),
|
|
181
181
|
"utf-8"
|
|
182
182
|
);
|
|
183
|
-
const envContent = `VITE_BASE_PATH=${options.basePath}
|
|
184
|
-
`;
|
|
185
|
-
await writeFile2(join3(tempDir, ".env"), envContent, "utf-8");
|
|
186
183
|
execSync("npm install --no-audit --no-fund", {
|
|
187
184
|
cwd: tempDir,
|
|
188
185
|
stdio: "pipe"
|
|
189
186
|
});
|
|
190
187
|
execSync("npx vite build", {
|
|
191
188
|
cwd: tempDir,
|
|
192
|
-
stdio: "pipe"
|
|
189
|
+
stdio: "pipe",
|
|
190
|
+
env: { ...process.env, VITE_BASE_PATH: options.basePath }
|
|
193
191
|
});
|
|
194
192
|
await rm(options.output, { recursive: true, force: true });
|
|
195
193
|
await mkdir2(options.output, { recursive: true });
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/init.ts","../../src/cli/templates/doxla-workflow.ts","../../src/cli/commands/build.ts","../../src/cli/lib/discover.ts","../../src/cli/lib/manifest.ts","../../src/cli/lib/build-app.ts","../../src/cli/lib/template.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { buildCommand } from \"./commands/build.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"doxla\")\n .description(\"Improve documentation discoverability within repos\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"init\")\n .description(\"Set up GitHub Actions workflow for Doxla docs deployment\")\n .action(initCommand);\n\nprogram\n .command(\"build\")\n .description(\"Discover markdown files and build the docs viewer\")\n .option(\"-o, --output <dir>\", \"Output directory\", \"doxla-dist\")\n .option(\"-r, --root <dir>\", \"Root directory to scan for markdown files\", \".\")\n .option(\"--base-path <path>\", \"Base path for GitHub Pages deployment\", \"/\")\n .action(buildCommand);\n\nprogram.parse();\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport chalk from \"chalk\";\nimport { workflowTemplate } from \"../templates/doxla-workflow.js\";\n\nexport async function initCommand() {\n const workflowDir = join(process.cwd(), \".github\", \"workflows\");\n const workflowPath = join(workflowDir, \"doxla.yml\");\n\n if (existsSync(workflowPath)) {\n console.log(\n chalk.yellow(\"⚠ Workflow file already exists at .github/workflows/doxla.yml\")\n );\n console.log(chalk.yellow(\" Overwriting with latest template...\"));\n }\n\n await mkdir(workflowDir, { recursive: true });\n await writeFile(workflowPath, workflowTemplate, \"utf-8\");\n\n console.log(chalk.green(\"✓ Created .github/workflows/doxla.yml\"));\n console.log();\n console.log(\"Next steps:\");\n console.log(\n ` 1. Enable GitHub Pages in your repo settings (Settings → Pages → Source: ${chalk.bold(\"GitHub Actions\")})`\n );\n console.log(\" 2. Commit and push the workflow file\");\n console.log(\n \" 3. Your docs will be built and deployed on every push to main\"\n );\n}\n","export const workflowTemplate = `name: Deploy Doxla Docs\n\non:\n push:\n branches: [main]\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\nconcurrency:\n group: \"pages\"\n cancel-in-progress: false\n\njobs:\n build-and-deploy:\n runs-on: ubuntu-latest\n environment:\n name: github-pages\n url: \\${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n - run: npx doxla@latest build --base-path \"/\\${{ github.event.repository.name }}\"\n - uses: actions/upload-pages-artifact@v3\n with:\n path: ./doxla-dist\n - id: deployment\n uses: actions/deploy-pages@v4\n`;\n","import { resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { discoverMarkdownFiles } from \"../lib/discover.js\";\nimport { generateManifest } from \"../lib/manifest.js\";\nimport { buildApp } from \"../lib/build-app.js\";\n\ninterface BuildOptions {\n output: string;\n root: string;\n basePath: string;\n}\n\nexport async function buildCommand(options: BuildOptions) {\n const rootDir = resolve(options.root);\n const outputDir = resolve(options.output);\n const basePath = options.basePath;\n\n console.log(chalk.bold(\"doxla build\"));\n console.log();\n\n // Step 1: Discover markdown files\n const discoverSpinner = ora(\"Discovering markdown files...\").start();\n const files = await discoverMarkdownFiles(rootDir);\n\n if (files.length === 0) {\n discoverSpinner.fail(\"No markdown files found\");\n process.exit(1);\n }\n\n discoverSpinner.succeed(`Found ${files.length} markdown file${files.length === 1 ? \"\" : \"s\"}`);\n\n // Step 2: Generate manifest\n const manifestSpinner = ora(\"Generating manifest...\").start();\n const manifest = await generateManifest(rootDir, files);\n manifestSpinner.succeed(\"Manifest generated\");\n\n // Step 3: Build docs viewer\n const buildSpinner = ora(\"Building docs viewer...\").start();\n try {\n await buildApp(manifest, { output: outputDir, basePath });\n buildSpinner.succeed(\"Docs viewer built\");\n } catch (error) {\n buildSpinner.fail(\"Build failed\");\n console.error(chalk.red((error as Error).message));\n process.exit(1);\n }\n\n console.log();\n console.log(chalk.green(`✓ Output written to ${options.output}/`));\n}\n","import fg from \"fast-glob\";\n\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/doxla-dist/**\",\n \"**/.next/**\",\n \"**/coverage/**\",\n];\n\nexport async function discoverMarkdownFiles(rootDir: string): Promise<string[]> {\n const files = await fg(\"**/*.md\", {\n cwd: rootDir,\n ignore: IGNORE_PATTERNS,\n onlyFiles: true,\n });\n\n // Sort: README.md first, then alphabetical\n return files.sort((a, b) => {\n const aIsReadme = a.toLowerCase() === \"readme.md\";\n const bIsReadme = b.toLowerCase() === \"readme.md\";\n if (aIsReadme && !bIsReadme) return -1;\n if (!aIsReadme && bIsReadme) return 1;\n return a.localeCompare(b);\n });\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join, basename, dirname } from \"node:path\";\nimport type { Manifest, DocFile } from \"../../app/src/types/manifest.js\";\n\nfunction extractTitle(content: string, filePath: string): string {\n // Try to extract from first # heading\n const match = content.match(/^#\\s+(.+)$/m);\n if (match) {\n return match[1].trim();\n }\n\n // Fallback to filename without extension\n const name = basename(filePath, \".md\");\n if (name.toLowerCase() === \"readme\") {\n const dir = dirname(filePath);\n if (dir === \".\") return \"README\";\n return `${dir} - README`;\n }\n return name.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction createSlug(filePath: string): string {\n return filePath\n .replace(/\\.md$/i, \"\")\n .replace(/\\\\/g, \"/\")\n .toLowerCase()\n .replace(/[^a-z0-9/.-]/g, \"-\");\n}\n\nexport async function generateManifest(\n rootDir: string,\n files: string[]\n): Promise<Manifest> {\n const docs: DocFile[] = await Promise.all(\n files.map(async (filePath) => {\n const fullPath = join(rootDir, filePath);\n const content = await readFile(fullPath, \"utf-8\");\n return {\n slug: createSlug(filePath),\n path: filePath,\n title: extractTitle(content, filePath),\n content,\n };\n })\n );\n\n const repoName = basename(rootDir);\n\n return {\n repoName,\n generatedAt: new Date().toISOString(),\n totalDocs: docs.length,\n docs,\n };\n}\n","import { cp, writeFile, rm, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport type { Manifest } from \"../../app/src/types/manifest.js\";\nimport { getTemplatePath } from \"./template.js\";\n\ninterface BuildOptions {\n output: string;\n basePath: string;\n}\n\nexport async function buildApp(\n manifest: Manifest,\n options: BuildOptions\n): Promise<void> {\n const templateDir = getTemplatePath();\n const tempDir = join(\n tmpdir(),\n `doxla-build-${randomBytes(6).toString(\"hex\")}`\n );\n\n try {\n // Copy template to temp dir\n await cp(templateDir, tempDir, { recursive: true });\n\n // Write manifest.json into the app's src directory\n await writeFile(\n join(tempDir, \"src\", \"manifest.json\"),\n JSON.stringify(manifest, null, 2),\n \"utf-8\"\n );\n\n // Write .env with base path\n const envContent = `VITE_BASE_PATH=${options.basePath}\\n`;\n await writeFile(join(tempDir, \".env\"), envContent, \"utf-8\");\n\n // Install dependencies\n execSync(\"npm install --no-audit --no-fund\", {\n cwd: tempDir,\n stdio: \"pipe\",\n });\n\n // Build with Vite\n execSync(\"npx vite build\", {\n cwd: tempDir,\n stdio: \"pipe\",\n });\n\n // Copy output\n await rm(options.output, { recursive: true, force: true });\n await mkdir(options.output, { recursive: true });\n await cp(join(tempDir, \"dist\"), options.output, { recursive: true });\n } finally {\n // Clean up temp dir\n await rm(tempDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nexport function getTemplatePath(): string {\n const __filename = fileURLToPath(import.meta.url);\n let dir = dirname(__filename);\n\n // Walk up from the current file location until we find src/app\n // Works both from source (src/cli/lib/) and built (dist/cli/)\n for (let i = 0; i < 5; i++) {\n const candidate = resolve(dir, \"src\", \"app\");\n if (existsSync(resolve(candidate, \"package.json\"))) {\n return candidate;\n }\n dir = dirname(dir);\n }\n\n // Fallback: assume built location dist/cli/ -> ../../src/app\n const fallback = resolve(dirname(__filename), \"..\", \"..\", \"src\", \"app\");\n return fallback;\n}\n"],"mappings":"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","names":["resolve","chalk","join","writeFile","mkdir","join","dirname","existsSync","join","writeFile","mkdir","resolve","chalk"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/init.ts","../../src/cli/templates/doxla-workflow.ts","../../src/cli/commands/build.ts","../../src/cli/lib/discover.ts","../../src/cli/lib/manifest.ts","../../src/cli/lib/build-app.ts","../../src/cli/lib/template.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { buildCommand } from \"./commands/build.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"doxla\")\n .description(\"Improve documentation discoverability within repos\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"init\")\n .description(\"Set up GitHub Actions workflow for Doxla docs deployment\")\n .action(initCommand);\n\nprogram\n .command(\"build\")\n .description(\"Discover markdown files and build the docs viewer\")\n .option(\"-o, --output <dir>\", \"Output directory\", \"doxla-dist\")\n .option(\"-r, --root <dir>\", \"Root directory to scan for markdown files\", \".\")\n .option(\"--base-path <path>\", \"Base path for GitHub Pages deployment\", \"/\")\n .action(buildCommand);\n\nprogram.parse();\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport chalk from \"chalk\";\nimport { workflowTemplate } from \"../templates/doxla-workflow.js\";\n\nexport async function initCommand() {\n const workflowDir = join(process.cwd(), \".github\", \"workflows\");\n const workflowPath = join(workflowDir, \"doxla.yml\");\n\n if (existsSync(workflowPath)) {\n console.log(\n chalk.yellow(\"⚠ Workflow file already exists at .github/workflows/doxla.yml\")\n );\n console.log(chalk.yellow(\" Overwriting with latest template...\"));\n }\n\n await mkdir(workflowDir, { recursive: true });\n await writeFile(workflowPath, workflowTemplate, \"utf-8\");\n\n console.log(chalk.green(\"✓ Created .github/workflows/doxla.yml\"));\n console.log();\n console.log(\"Next steps:\");\n console.log(\n ` 1. Enable GitHub Pages in your repo settings (Settings → Pages → Source: ${chalk.bold(\"GitHub Actions\")})`\n );\n console.log(\" 2. Commit and push the workflow file\");\n console.log(\n \" 3. Your docs will be built and deployed on every push to main\"\n );\n}\n","export const workflowTemplate = `name: Deploy Doxla Docs\n\non:\n push:\n branches: [main]\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\nconcurrency:\n group: \"pages\"\n cancel-in-progress: false\n\njobs:\n build-and-deploy:\n runs-on: ubuntu-latest\n environment:\n name: github-pages\n url: \\${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n - run: npx doxla@latest build --base-path \"/\\${{ github.event.repository.name }}\"\n - uses: actions/upload-pages-artifact@v3\n with:\n path: ./doxla-dist\n - id: deployment\n uses: actions/deploy-pages@v4\n`;\n","import { resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { discoverMarkdownFiles } from \"../lib/discover.js\";\nimport { generateManifest } from \"../lib/manifest.js\";\nimport { buildApp } from \"../lib/build-app.js\";\n\ninterface BuildOptions {\n output: string;\n root: string;\n basePath: string;\n}\n\nexport async function buildCommand(options: BuildOptions) {\n const rootDir = resolve(options.root);\n const outputDir = resolve(options.output);\n const basePath = options.basePath;\n\n console.log(chalk.bold(\"doxla build\"));\n console.log();\n\n // Step 1: Discover markdown files\n const discoverSpinner = ora(\"Discovering markdown files...\").start();\n const files = await discoverMarkdownFiles(rootDir);\n\n if (files.length === 0) {\n discoverSpinner.fail(\"No markdown files found\");\n process.exit(1);\n }\n\n discoverSpinner.succeed(`Found ${files.length} markdown file${files.length === 1 ? \"\" : \"s\"}`);\n\n // Step 2: Generate manifest\n const manifestSpinner = ora(\"Generating manifest...\").start();\n const manifest = await generateManifest(rootDir, files);\n manifestSpinner.succeed(\"Manifest generated\");\n\n // Step 3: Build docs viewer\n const buildSpinner = ora(\"Building docs viewer...\").start();\n try {\n await buildApp(manifest, { output: outputDir, basePath });\n buildSpinner.succeed(\"Docs viewer built\");\n } catch (error) {\n buildSpinner.fail(\"Build failed\");\n console.error(chalk.red((error as Error).message));\n process.exit(1);\n }\n\n console.log();\n console.log(chalk.green(`✓ Output written to ${options.output}/`));\n}\n","import fg from \"fast-glob\";\n\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/doxla-dist/**\",\n \"**/.next/**\",\n \"**/coverage/**\",\n];\n\nexport async function discoverMarkdownFiles(rootDir: string): Promise<string[]> {\n const files = await fg(\"**/*.md\", {\n cwd: rootDir,\n ignore: IGNORE_PATTERNS,\n onlyFiles: true,\n });\n\n // Sort: README.md first, then alphabetical\n return files.sort((a, b) => {\n const aIsReadme = a.toLowerCase() === \"readme.md\";\n const bIsReadme = b.toLowerCase() === \"readme.md\";\n if (aIsReadme && !bIsReadme) return -1;\n if (!aIsReadme && bIsReadme) return 1;\n return a.localeCompare(b);\n });\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join, basename, dirname } from \"node:path\";\nimport type { Manifest, DocFile } from \"../../app/src/types/manifest.js\";\n\nfunction extractTitle(content: string, filePath: string): string {\n // Try to extract from first # heading\n const match = content.match(/^#\\s+(.+)$/m);\n if (match) {\n return match[1].trim();\n }\n\n // Fallback to filename without extension\n const name = basename(filePath, \".md\");\n if (name.toLowerCase() === \"readme\") {\n const dir = dirname(filePath);\n if (dir === \".\") return \"README\";\n return `${dir} - README`;\n }\n return name.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction createSlug(filePath: string): string {\n return filePath\n .replace(/\\.md$/i, \"\")\n .replace(/\\\\/g, \"/\")\n .toLowerCase()\n .replace(/[^a-z0-9/.-]/g, \"-\");\n}\n\nexport async function generateManifest(\n rootDir: string,\n files: string[]\n): Promise<Manifest> {\n const docs: DocFile[] = await Promise.all(\n files.map(async (filePath) => {\n const fullPath = join(rootDir, filePath);\n const content = await readFile(fullPath, \"utf-8\");\n return {\n slug: createSlug(filePath),\n path: filePath,\n title: extractTitle(content, filePath),\n content,\n };\n })\n );\n\n const repoName = basename(rootDir);\n\n return {\n repoName,\n generatedAt: new Date().toISOString(),\n totalDocs: docs.length,\n docs,\n };\n}\n","import { cp, writeFile, rm, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport type { Manifest } from \"../../app/src/types/manifest.js\";\nimport { getTemplatePath } from \"./template.js\";\n\ninterface BuildOptions {\n output: string;\n basePath: string;\n}\n\nexport async function buildApp(\n manifest: Manifest,\n options: BuildOptions\n): Promise<void> {\n const templateDir = getTemplatePath();\n const tempDir = join(\n tmpdir(),\n `doxla-build-${randomBytes(6).toString(\"hex\")}`\n );\n\n try {\n // Copy template to temp dir\n await cp(templateDir, tempDir, { recursive: true });\n\n // Write manifest.json into the app's src directory\n await writeFile(\n join(tempDir, \"src\", \"manifest.json\"),\n JSON.stringify(manifest, null, 2),\n \"utf-8\"\n );\n\n // Install dependencies\n execSync(\"npm install --no-audit --no-fund\", {\n cwd: tempDir,\n stdio: \"pipe\",\n });\n\n // Build with Vite, passing base path as env var\n execSync(\"npx vite build\", {\n cwd: tempDir,\n stdio: \"pipe\",\n env: { ...process.env, VITE_BASE_PATH: options.basePath },\n });\n\n // Copy output\n await rm(options.output, { recursive: true, force: true });\n await mkdir(options.output, { recursive: true });\n await cp(join(tempDir, \"dist\"), options.output, { recursive: true });\n } finally {\n // Clean up temp dir\n await rm(tempDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nexport function getTemplatePath(): string {\n const __filename = fileURLToPath(import.meta.url);\n let dir = dirname(__filename);\n\n // Walk up from the current file location until we find src/app\n // Works both from source (src/cli/lib/) and built (dist/cli/)\n for (let i = 0; i < 5; i++) {\n const candidate = resolve(dir, \"src\", \"app\");\n if (existsSync(resolve(candidate, \"package.json\"))) {\n return candidate;\n }\n dir = dirname(dir);\n }\n\n // Fallback: assume built location dist/cli/ -> ../../src/app\n const fallback = resolve(dirname(__filename), \"..\", \"..\", \"src\", \"app\");\n return fallback;\n}\n"],"mappings":";;;AAAA,SAAS,eAAe;;;ACAxB,SAAS,OAAO,iBAAiB;AACjC,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,OAAO,WAAW;;;ACHX,IAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADMhC,eAAsB,cAAc;AAClC,QAAM,cAAc,KAAK,QAAQ,IAAI,GAAG,WAAW,WAAW;AAC9D,QAAM,eAAe,KAAK,aAAa,WAAW;AAElD,MAAI,WAAW,YAAY,GAAG;AAC5B,YAAQ;AAAA,MACN,MAAM,OAAO,oEAA+D;AAAA,IAC9E;AACA,YAAQ,IAAI,MAAM,OAAO,uCAAuC,CAAC;AAAA,EACnE;AAEA,QAAM,MAAM,aAAa,EAAE,WAAW,KAAK,CAAC;AAC5C,QAAM,UAAU,cAAc,kBAAkB,OAAO;AAEvD,UAAQ,IAAI,MAAM,MAAM,4CAAuC,CAAC;AAChE,UAAQ,IAAI;AACZ,UAAQ,IAAI,aAAa;AACzB,UAAQ;AAAA,IACN,wFAA8E,MAAM,KAAK,gBAAgB,CAAC;AAAA,EAC5G;AACA,UAAQ,IAAI,wCAAwC;AACpD,UAAQ;AAAA,IACN;AAAA,EACF;AACF;;;AE9BA,SAAS,WAAAA,gBAAe;AACxB,OAAOC,YAAW;AAClB,OAAO,SAAS;;;ACFhB,OAAO,QAAQ;AAEf,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,eAAsB,sBAAsB,SAAoC;AAC9E,QAAM,QAAQ,MAAM,GAAG,WAAW;AAAA,IAChC,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,WAAW;AAAA,EACb,CAAC;AAGD,SAAO,MAAM,KAAK,CAAC,GAAG,MAAM;AAC1B,UAAM,YAAY,EAAE,YAAY,MAAM;AACtC,UAAM,YAAY,EAAE,YAAY,MAAM;AACtC,QAAI,aAAa,CAAC,UAAW,QAAO;AACpC,QAAI,CAAC,aAAa,UAAW,QAAO;AACpC,WAAO,EAAE,cAAc,CAAC;AAAA,EAC1B,CAAC;AACH;;;AC3BA,SAAS,gBAAgB;AACzB,SAAS,QAAAC,OAAM,UAAU,eAAe;AAGxC,SAAS,aAAa,SAAiB,UAA0B;AAE/D,QAAM,QAAQ,QAAQ,MAAM,aAAa;AACzC,MAAI,OAAO;AACT,WAAO,MAAM,CAAC,EAAE,KAAK;AAAA,EACvB;AAGA,QAAM,OAAO,SAAS,UAAU,KAAK;AACrC,MAAI,KAAK,YAAY,MAAM,UAAU;AACnC,UAAM,MAAM,QAAQ,QAAQ;AAC5B,QAAI,QAAQ,IAAK,QAAO;AACxB,WAAO,GAAG,GAAG;AAAA,EACf;AACA,SAAO,KAAK,QAAQ,SAAS,GAAG,EAAE,QAAQ,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC;AAC3E;AAEA,SAAS,WAAW,UAA0B;AAC5C,SAAO,SACJ,QAAQ,UAAU,EAAE,EACpB,QAAQ,OAAO,GAAG,EAClB,YAAY,EACZ,QAAQ,iBAAiB,GAAG;AACjC;AAEA,eAAsB,iBACpB,SACA,OACmB;AACnB,QAAM,OAAkB,MAAM,QAAQ;AAAA,IACpC,MAAM,IAAI,OAAO,aAAa;AAC5B,YAAM,WAAWA,MAAK,SAAS,QAAQ;AACvC,YAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,aAAO;AAAA,QACL,MAAM,WAAW,QAAQ;AAAA,QACzB,MAAM;AAAA,QACN,OAAO,aAAa,SAAS,QAAQ;AAAA,QACrC;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,WAAW,SAAS,OAAO;AAEjC,SAAO;AAAA,IACL;AAAA,IACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,WAAW,KAAK;AAAA,IAChB;AAAA,EACF;AACF;;;ACtDA,SAAS,IAAI,aAAAC,YAAW,IAAI,SAAAC,cAAa;AACzC,SAAS,QAAAC,aAAY;AACrB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;;;ACJ5B,SAAS,qBAAqB;AAC9B,SAAS,WAAAC,UAAS,eAAe;AACjC,SAAS,cAAAC,mBAAkB;AAEpB,SAAS,kBAA0B;AACxC,QAAM,aAAa,cAAc,YAAY,GAAG;AAChD,MAAI,MAAMD,SAAQ,UAAU;AAI5B,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,YAAY,QAAQ,KAAK,OAAO,KAAK;AAC3C,QAAIC,YAAW,QAAQ,WAAW,cAAc,CAAC,GAAG;AAClD,aAAO;AAAA,IACT;AACA,UAAMD,SAAQ,GAAG;AAAA,EACnB;AAGA,QAAM,WAAW,QAAQA,SAAQ,UAAU,GAAG,MAAM,MAAM,OAAO,KAAK;AACtE,SAAO;AACT;;;ADRA,eAAsB,SACpB,UACA,SACe;AACf,QAAM,cAAc,gBAAgB;AACpC,QAAM,UAAUE;AAAA,IACd,OAAO;AAAA,IACP,eAAe,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;AAAA,EAC/C;AAEA,MAAI;AAEF,UAAM,GAAG,aAAa,SAAS,EAAE,WAAW,KAAK,CAAC;AAGlD,UAAMC;AAAA,MACJD,MAAK,SAAS,OAAO,eAAe;AAAA,MACpC,KAAK,UAAU,UAAU,MAAM,CAAC;AAAA,MAChC;AAAA,IACF;AAGA,aAAS,oCAAoC;AAAA,MAC3C,KAAK;AAAA,MACL,OAAO;AAAA,IACT,CAAC;AAGD,aAAS,kBAAkB;AAAA,MACzB,KAAK;AAAA,MACL,OAAO;AAAA,MACP,KAAK,EAAE,GAAG,QAAQ,KAAK,gBAAgB,QAAQ,SAAS;AAAA,IAC1D,CAAC;AAGD,UAAM,GAAG,QAAQ,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACzD,UAAME,OAAM,QAAQ,QAAQ,EAAE,WAAW,KAAK,CAAC;AAC/C,UAAM,GAAGF,MAAK,SAAS,MAAM,GAAG,QAAQ,QAAQ,EAAE,WAAW,KAAK,CAAC;AAAA,EACrE,UAAE;AAEA,UAAM,GAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACpE;AACF;;;AH1CA,eAAsB,aAAa,SAAuB;AACxD,QAAM,UAAUG,SAAQ,QAAQ,IAAI;AACpC,QAAM,YAAYA,SAAQ,QAAQ,MAAM;AACxC,QAAM,WAAW,QAAQ;AAEzB,UAAQ,IAAIC,OAAM,KAAK,aAAa,CAAC;AACrC,UAAQ,IAAI;AAGZ,QAAM,kBAAkB,IAAI,+BAA+B,EAAE,MAAM;AACnE,QAAM,QAAQ,MAAM,sBAAsB,OAAO;AAEjD,MAAI,MAAM,WAAW,GAAG;AACtB,oBAAgB,KAAK,yBAAyB;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,kBAAgB,QAAQ,SAAS,MAAM,MAAM,iBAAiB,MAAM,WAAW,IAAI,KAAK,GAAG,EAAE;AAG7F,QAAM,kBAAkB,IAAI,wBAAwB,EAAE,MAAM;AAC5D,QAAM,WAAW,MAAM,iBAAiB,SAAS,KAAK;AACtD,kBAAgB,QAAQ,oBAAoB;AAG5C,QAAM,eAAe,IAAI,yBAAyB,EAAE,MAAM;AAC1D,MAAI;AACF,UAAM,SAAS,UAAU,EAAE,QAAQ,WAAW,SAAS,CAAC;AACxD,iBAAa,QAAQ,mBAAmB;AAAA,EAC1C,SAAS,OAAO;AACd,iBAAa,KAAK,cAAc;AAChC,YAAQ,MAAMA,OAAM,IAAK,MAAgB,OAAO,CAAC;AACjD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,IAAI;AACZ,UAAQ,IAAIA,OAAM,MAAM,4BAAuB,QAAQ,MAAM,GAAG,CAAC;AACnE;;;AH9CA,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,OAAO,EACZ,YAAY,oDAAoD,EAChE,QAAQ,OAAO;AAElB,QACG,QAAQ,MAAM,EACd,YAAY,0DAA0D,EACtE,OAAO,WAAW;AAErB,QACG,QAAQ,OAAO,EACf,YAAY,mDAAmD,EAC/D,OAAO,sBAAsB,oBAAoB,YAAY,EAC7D,OAAO,oBAAoB,6CAA6C,GAAG,EAC3E,OAAO,sBAAsB,yCAAyC,GAAG,EACzE,OAAO,YAAY;AAEtB,QAAQ,MAAM;","names":["resolve","chalk","join","writeFile","mkdir","join","dirname","existsSync","join","writeFile","mkdir","resolve","chalk"]}
|
package/package.json
CHANGED
package/src/app/src/App.tsx
CHANGED
|
@@ -6,6 +6,18 @@ import { IndexPage } from "./components/IndexPage";
|
|
|
6
6
|
import { DocPage } from "./components/DocPage";
|
|
7
7
|
import { SearchResults } from "./components/SearchResults";
|
|
8
8
|
|
|
9
|
+
export type Theme = "light" | "dark";
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = "doxla-theme";
|
|
12
|
+
|
|
13
|
+
function getInitialTheme(): Theme {
|
|
14
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
15
|
+
if (stored === "light" || stored === "dark") return stored;
|
|
16
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
17
|
+
? "dark"
|
|
18
|
+
: "light";
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
const data = manifest as Manifest;
|
|
10
22
|
|
|
11
23
|
type Route =
|
|
@@ -34,6 +46,20 @@ function parseHash(): Route {
|
|
|
34
46
|
|
|
35
47
|
export default function App() {
|
|
36
48
|
const [route, setRoute] = useState<Route>(parseHash);
|
|
49
|
+
const [theme, setTheme] = useState<Theme>(getInitialTheme);
|
|
50
|
+
|
|
51
|
+
const toggleTheme = useCallback(() => {
|
|
52
|
+
setTheme((prev) => {
|
|
53
|
+
const next = prev === "light" ? "dark" : "light";
|
|
54
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
55
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
56
|
+
return next;
|
|
57
|
+
});
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
62
|
+
}, [theme]);
|
|
37
63
|
|
|
38
64
|
const handleHashChange = useCallback(() => {
|
|
39
65
|
setRoute(parseHash());
|
|
@@ -63,7 +89,7 @@ export default function App() {
|
|
|
63
89
|
</div>
|
|
64
90
|
);
|
|
65
91
|
}
|
|
66
|
-
return <DocPage doc={doc} />;
|
|
92
|
+
return <DocPage doc={doc} theme={theme} />;
|
|
67
93
|
}
|
|
68
94
|
case "search":
|
|
69
95
|
return <SearchResults docs={data.docs} query={route.query} />;
|
|
@@ -71,6 +97,8 @@ export default function App() {
|
|
|
71
97
|
};
|
|
72
98
|
|
|
73
99
|
return (
|
|
74
|
-
<Layout manifest={data}
|
|
100
|
+
<Layout manifest={data} theme={theme} onToggleTheme={toggleTheme}>
|
|
101
|
+
{renderContent()}
|
|
102
|
+
</Layout>
|
|
75
103
|
);
|
|
76
104
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { DocFile } from "../types/manifest";
|
|
2
|
+
import type { Theme } from "../App";
|
|
2
3
|
import { MarkdownRenderer } from "./MarkdownRenderer";
|
|
3
4
|
import { Badge } from "./ui/Badge";
|
|
4
5
|
import { Separator } from "./ui/Separator";
|
|
5
6
|
|
|
6
7
|
interface DocPageProps {
|
|
7
8
|
doc: DocFile;
|
|
9
|
+
theme: Theme;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export function DocPage({ doc }: DocPageProps) {
|
|
12
|
+
export function DocPage({ doc, theme }: DocPageProps) {
|
|
11
13
|
const pathParts = doc.path.split("/");
|
|
12
14
|
|
|
13
15
|
return (
|
|
@@ -35,7 +37,7 @@ export function DocPage({ doc }: DocPageProps) {
|
|
|
35
37
|
|
|
36
38
|
<Separator className="mb-6" />
|
|
37
39
|
|
|
38
|
-
<MarkdownRenderer content={doc.content} />
|
|
40
|
+
<MarkdownRenderer content={doc.content} theme={theme} />
|
|
39
41
|
</div>
|
|
40
42
|
);
|
|
41
43
|
}
|
|
@@ -1,44 +1,52 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
1
2
|
import Markdown from "react-markdown";
|
|
2
3
|
import remarkGfm from "remark-gfm";
|
|
3
4
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
4
|
-
import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
5
|
+
import { oneLight, oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
5
6
|
import type { Components } from "react-markdown";
|
|
7
|
+
import type { Theme } from "../App";
|
|
6
8
|
|
|
7
9
|
interface MarkdownRendererProps {
|
|
8
10
|
content: string;
|
|
11
|
+
theme: Theme;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { children, className, ...rest } = props;
|
|
14
|
-
const match = /language-(\w+)/.exec(className || "");
|
|
15
|
-
const isInline = !match;
|
|
14
|
+
export function MarkdownRenderer({ content, theme }: MarkdownRendererProps) {
|
|
15
|
+
const syntaxStyle = theme === "dark" ? oneDark : oneLight;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{children}
|
|
24
|
-
</code>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
17
|
+
const components: Components = useMemo(
|
|
18
|
+
() => ({
|
|
19
|
+
code(props) {
|
|
20
|
+
const { children, className, ...rest } = props;
|
|
21
|
+
const match = /language-(\w+)/.exec(className || "");
|
|
22
|
+
const isInline = !match;
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
if (isInline) {
|
|
25
|
+
return (
|
|
26
|
+
<code
|
|
27
|
+
className="rounded bg-muted px-1.5 py-0.5 text-sm font-mono"
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</code>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<SyntaxHighlighter
|
|
37
|
+
style={syntaxStyle}
|
|
38
|
+
language={match[1]}
|
|
39
|
+
PreTag="div"
|
|
40
|
+
className="rounded-md text-sm"
|
|
41
|
+
>
|
|
42
|
+
{String(children).replace(/\n$/, "")}
|
|
43
|
+
</SyntaxHighlighter>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
[syntaxStyle],
|
|
48
|
+
);
|
|
40
49
|
|
|
41
|
-
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
|
42
50
|
return (
|
|
43
51
|
<div className="prose prose-neutral max-w-none">
|
|
44
52
|
<Markdown remarkPlugins={[remarkGfm]} components={components}>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import { Search, BookOpen } from "lucide-react";
|
|
2
|
+
import { Search, BookOpen, Sun, Moon } from "lucide-react";
|
|
3
|
+
import type { Theme } from "../../App";
|
|
3
4
|
import { Input } from "../ui/Input";
|
|
4
5
|
import { Button } from "../ui/Button";
|
|
5
6
|
|
|
6
7
|
interface HeaderProps {
|
|
7
8
|
repoName: string;
|
|
9
|
+
theme: Theme;
|
|
10
|
+
onToggleTheme: () => void;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
export function Header({ repoName }: HeaderProps) {
|
|
13
|
+
export function Header({ repoName, theme, onToggleTheme }: HeaderProps) {
|
|
11
14
|
const [searchInput, setSearchInput] = useState("");
|
|
12
15
|
|
|
13
16
|
const handleSearch = (e: React.FormEvent) => {
|
|
@@ -39,6 +42,15 @@ export function Header({ repoName }: HeaderProps) {
|
|
|
39
42
|
Search
|
|
40
43
|
</Button>
|
|
41
44
|
</form>
|
|
45
|
+
|
|
46
|
+
<Button
|
|
47
|
+
size="sm"
|
|
48
|
+
variant="ghost"
|
|
49
|
+
onClick={onToggleTheme}
|
|
50
|
+
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
51
|
+
>
|
|
52
|
+
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
|
53
|
+
</Button>
|
|
42
54
|
</header>
|
|
43
55
|
);
|
|
44
56
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { Manifest } from "../../types/manifest";
|
|
3
|
+
import type { Theme } from "../../App";
|
|
3
4
|
import { Header } from "./Header";
|
|
4
5
|
import { Sidebar } from "./Sidebar";
|
|
5
6
|
|
|
6
7
|
interface LayoutProps {
|
|
7
8
|
manifest: Manifest;
|
|
9
|
+
theme: Theme;
|
|
10
|
+
onToggleTheme: () => void;
|
|
8
11
|
children: ReactNode;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
export function Layout({ manifest, children }: LayoutProps) {
|
|
14
|
+
export function Layout({ manifest, theme, onToggleTheme, children }: LayoutProps) {
|
|
12
15
|
return (
|
|
13
16
|
<div className="min-h-screen">
|
|
14
|
-
<Header repoName={manifest.repoName} />
|
|
17
|
+
<Header repoName={manifest.repoName} theme={theme} onToggleTheme={onToggleTheme} />
|
|
15
18
|
<div className="flex">
|
|
16
19
|
<Sidebar docs={manifest.docs} />
|
|
17
20
|
<main className="flex-1 overflow-auto">
|
package/src/app/src/globals.css
CHANGED
|
@@ -16,6 +16,19 @@
|
|
|
16
16
|
--radius-lg: 0.5rem;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
[data-theme="dark"] {
|
|
20
|
+
color-scheme: dark;
|
|
21
|
+
--color-background: #0a0a0a;
|
|
22
|
+
--color-foreground: #fafafa;
|
|
23
|
+
--color-muted: #262626;
|
|
24
|
+
--color-muted-foreground: #a3a3a3;
|
|
25
|
+
--color-border: #2e2e2e;
|
|
26
|
+
--color-primary: #fafafa;
|
|
27
|
+
--color-primary-foreground: #171717;
|
|
28
|
+
--color-accent: #262626;
|
|
29
|
+
--color-accent-foreground: #fafafa;
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
body {
|
|
20
33
|
font-family:
|
|
21
34
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|