@docubook/flame 1.1.0 → 1.2.1
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/.docu/node/build.ts +6 -1
- package/.docu/node/html.ts +4 -2
- package/.docu/node/hydrate.ts +65 -1
- package/.docu/node/preview.ts +27 -16
- package/.docu/node/security.ts +34 -4
- package/.docu/node/server.ts +11 -7
- package/.docu/node/types.ts +5 -0
- package/README.md +93 -0
- package/bin/cli.js +8 -1
- package/docu.schema.json +29 -0
- package/package.json +4 -3
- package/template/.env.example +5 -0
- package/template/docu.json +3 -0
package/.docu/node/build.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./paths";
|
|
17
17
|
import { htmlShell as createHtmlShell } from "./html";
|
|
18
18
|
import { generateSearchIndex } from "./search-indexer";
|
|
19
|
-
import { buildClientBundle } from "./hydrate";
|
|
19
|
+
import { buildClientBundle, computeInlineThemeCss } from "./hydrate";
|
|
20
20
|
import { logger } from "./logger";
|
|
21
21
|
import { initSentry, captureException } from "./sentry";
|
|
22
22
|
import type { BuildCache, CliArgs } from "./types";
|
|
@@ -95,6 +95,8 @@ export function shouldRebuild(path: string, mtime: number, cache: BuildCache): b
|
|
|
95
95
|
|
|
96
96
|
let assetManifest = { js: "client.js", css: "client.css" };
|
|
97
97
|
|
|
98
|
+
let inlineThemeCss: string | undefined;
|
|
99
|
+
|
|
98
100
|
function htmlShell(title: string, description: string, body: string): string {
|
|
99
101
|
const favicon = docuConfig.meta?.favicon || "/favicon.ico";
|
|
100
102
|
return createHtmlShell({
|
|
@@ -104,6 +106,7 @@ function htmlShell(title: string, description: string, body: string): string {
|
|
|
104
106
|
favicon,
|
|
105
107
|
css: assetManifest.css,
|
|
106
108
|
js: assetManifest.js,
|
|
109
|
+
themeCss: inlineThemeCss,
|
|
107
110
|
});
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -189,6 +192,8 @@ async function build() {
|
|
|
189
192
|
assetManifest = await buildClientBundle();
|
|
190
193
|
logger.bundleDone(Math.round(performance.now() - t));
|
|
191
194
|
|
|
195
|
+
inlineThemeCss = computeInlineThemeCss();
|
|
196
|
+
|
|
192
197
|
const lastManifest = cache["__assets__"];
|
|
193
198
|
const assetsChanged =
|
|
194
199
|
!lastManifest || lastManifest.hash !== `${assetManifest.js}:${assetManifest.css}`;
|
package/.docu/node/html.ts
CHANGED
|
@@ -7,11 +7,13 @@ export interface HtmlShellOptions {
|
|
|
7
7
|
js: string;
|
|
8
8
|
nonce?: string;
|
|
9
9
|
extraScripts?: string;
|
|
10
|
+
themeCss?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
13
|
-
const { title, description, body, favicon, css, js, nonce, extraScripts } = opts;
|
|
14
|
+
const { title, description, body, favicon, css, js, nonce, extraScripts, themeCss } = opts;
|
|
14
15
|
const nonceAttr = nonce ? ` nonce="${Bun.escapeHTML(nonce)}"` : "";
|
|
16
|
+
const themeStyle = themeCss ? `\n <style${nonceAttr}>${Bun.escapeHTML(themeCss)}</style>` : "";
|
|
15
17
|
return `<!DOCTYPE html>
|
|
16
18
|
<html lang="en">
|
|
17
19
|
<head>
|
|
@@ -19,7 +21,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
19
21
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20
22
|
<title>${Bun.escapeHTML(title)}</title>
|
|
21
23
|
<meta name="description" content="${Bun.escapeHTML(description)}">
|
|
22
|
-
<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(favicon)}"
|
|
24
|
+
<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(favicon)}">${themeStyle}
|
|
23
25
|
<link rel="stylesheet" href="/assets/${Bun.escapeHTML(css)}">
|
|
24
26
|
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>
|
|
25
27
|
</head>
|
package/.docu/node/hydrate.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { mkdir, readdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { resolveTheme, generateThemeCss, presetRegistry } from "@docubook/themes-colors";
|
|
3
4
|
import { ASSETS_DIR, LIB_DIR, STYLES_DIR, loadDocuConfig } from "./paths";
|
|
4
5
|
import { resolveRoutes } from "./fs-scanner";
|
|
5
6
|
import type { DocuRoute } from "./types";
|
|
7
|
+
import type { ThemeConfig } from "@docubook/themes-colors";
|
|
8
|
+
|
|
9
|
+
const themeRegistry = presetRegistry;
|
|
6
10
|
|
|
7
11
|
async function cleanOldBundles() {
|
|
8
12
|
try {
|
|
@@ -19,6 +23,53 @@ async function cleanOldBundles() {
|
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Read the effective theme config with this priority:
|
|
28
|
+
* 1. FLAME_THEME env var (CLI --theme flag)
|
|
29
|
+
* 2. docu.json theme.colors field
|
|
30
|
+
*/
|
|
31
|
+
export function getThemeConfig(): ThemeConfig | undefined {
|
|
32
|
+
if (process.env.FLAME_THEME) {
|
|
33
|
+
return process.env.FLAME_THEME;
|
|
34
|
+
}
|
|
35
|
+
const config = loadDocuConfig();
|
|
36
|
+
return config.themes?.colors;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Append theme CSS to compiled Tailwind output based on theme config.
|
|
41
|
+
*/
|
|
42
|
+
export function buildThemeCss(baseCss: string, themeConfig: unknown): string {
|
|
43
|
+
try {
|
|
44
|
+
const resolved = resolveTheme(themeConfig as ThemeConfig | undefined | null, themeRegistry);
|
|
45
|
+
return baseCss + "\n" + generateThemeCss(resolved);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`[flame] Failed to resolve theme CSS: ${err instanceof Error ? err.message : String(err)}`
|
|
49
|
+
);
|
|
50
|
+
return baseCss;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compute inline theme CSS for FOUC prevention.
|
|
56
|
+
* Returns undefined if no theme is configured or on error.
|
|
57
|
+
*/
|
|
58
|
+
export function computeInlineThemeCss(): string | undefined {
|
|
59
|
+
try {
|
|
60
|
+
const themeColors = getThemeConfig();
|
|
61
|
+
if (themeColors) {
|
|
62
|
+
const resolved = resolveTheme(themeColors, themeRegistry);
|
|
63
|
+
return generateThemeCss(resolved);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn(
|
|
67
|
+
`[flame] Failed to compute inline theme CSS: ${err instanceof Error ? err.message : String(err)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
22
73
|
export async function buildClientBundle(): Promise<{ js: string; css: string }> {
|
|
23
74
|
await mkdir(ASSETS_DIR, { recursive: true });
|
|
24
75
|
await cleanOldBundles();
|
|
@@ -93,7 +144,20 @@ export async function buildClientBundle(): Promise<{ js: string; css: string }>
|
|
|
93
144
|
const err = await new Response(proc.stderr).text();
|
|
94
145
|
throw new Error(`Tailwind CSS build failed:\n${err}`);
|
|
95
146
|
}
|
|
96
|
-
|
|
147
|
+
|
|
148
|
+
let cssContent = await Bun.file(tmpCss).text();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const themeColors = getThemeConfig();
|
|
152
|
+
if (themeColors) {
|
|
153
|
+
cssContent = buildThemeCss(cssContent, themeColors);
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[flame] Failed to resolve theme config, falling back to globals.css only: ${err instanceof Error ? err.message : String(err)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
97
161
|
const cssHash = new Bun.CryptoHasher("md5").update(cssContent).digest("hex").slice(0, 8);
|
|
98
162
|
const cssFile = `client-${cssHash}.css`;
|
|
99
163
|
await Bun.write(join(ASSETS_DIR, cssFile), cssContent);
|
package/.docu/node/preview.ts
CHANGED
|
@@ -3,19 +3,10 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { logger } from "./logger";
|
|
4
4
|
import { DIST_DIR } from "./paths";
|
|
5
5
|
import { getContentType } from "./utils";
|
|
6
|
+
import { SECURITY_HEADERS, generateNonce, cspHeader, injectNonce } from "./security";
|
|
6
7
|
|
|
7
8
|
const PORT = process.env.PORT || "4173";
|
|
8
9
|
|
|
9
|
-
const SECURITY_HEADERS: Record<string, string> = {
|
|
10
|
-
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
11
|
-
"X-Frame-Options": "DENY",
|
|
12
|
-
"X-Content-Type-Options": "nosniff",
|
|
13
|
-
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
14
|
-
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
|
15
|
-
"Content-Security-Policy":
|
|
16
|
-
"default-src 'self'; script-src 'self' 'sha256-4XrQ+xtH2goQdpEv7dRmUWACF3uhF9KRPpgayFex7QU=' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
10
|
logger.buildStart();
|
|
20
11
|
|
|
21
12
|
if (!existsSync(DIST_DIR)) {
|
|
@@ -50,22 +41,42 @@ const notFoundPath = resolve(DIST_DIR, "404.html");
|
|
|
50
41
|
const server = Bun.serve({
|
|
51
42
|
port: PORT,
|
|
52
43
|
|
|
53
|
-
fetch(req) {
|
|
44
|
+
async fetch(req) {
|
|
54
45
|
const url = new URL(req.url);
|
|
55
46
|
const pathname = decodeURIComponent(url.pathname);
|
|
56
47
|
|
|
57
48
|
const filePath = resolveFile(pathname);
|
|
49
|
+
|
|
58
50
|
if (filePath) {
|
|
59
51
|
const contentType = getContentType(filePath);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
if (contentType === "text/html") {
|
|
53
|
+
const nonce = generateNonce();
|
|
54
|
+
const html = await Bun.file(filePath).text();
|
|
55
|
+
const modified = injectNonce(html, nonce);
|
|
56
|
+
return new Response(modified, {
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "text/html",
|
|
59
|
+
...SECURITY_HEADERS,
|
|
60
|
+
"Content-Security-Policy": cspHeader(nonce, true),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return new Response(Bun.file(filePath), {
|
|
65
|
+
headers: { "Content-Type": contentType },
|
|
66
|
+
});
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
if (existsSync(notFoundPath)) {
|
|
66
|
-
|
|
70
|
+
const nonce = generateNonce();
|
|
71
|
+
const html = await Bun.file(notFoundPath).text();
|
|
72
|
+
const modified = injectNonce(html, nonce);
|
|
73
|
+
return new Response(modified, {
|
|
67
74
|
status: 404,
|
|
68
|
-
headers: {
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "text/html",
|
|
77
|
+
...SECURITY_HEADERS,
|
|
78
|
+
"Content-Security-Policy": cspHeader(nonce, true),
|
|
79
|
+
},
|
|
69
80
|
});
|
|
70
81
|
}
|
|
71
82
|
|
package/.docu/node/security.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
1
3
|
export const SECURITY_HEADERS: Record<string, string> = {
|
|
2
4
|
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
3
5
|
"X-Frame-Options": "DENY",
|
|
@@ -10,10 +12,13 @@ export function generateNonce(): string {
|
|
|
10
12
|
return crypto.randomUUID();
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function cspHeader(nonce: string): string {
|
|
15
|
+
export function cspHeader(nonce: string, allowEval = false): string {
|
|
16
|
+
const scriptSrc = allowEval
|
|
17
|
+
? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'`
|
|
18
|
+
: `script-src 'self' 'nonce-${nonce}'`;
|
|
14
19
|
return [
|
|
15
20
|
"default-src 'self'",
|
|
16
|
-
|
|
21
|
+
scriptSrc,
|
|
17
22
|
"style-src 'self' 'unsafe-inline'",
|
|
18
23
|
"img-src 'self' https: data:",
|
|
19
24
|
"font-src 'self' data:",
|
|
@@ -23,13 +28,38 @@ export function cspHeader(nonce: string): string {
|
|
|
23
28
|
].join("; ");
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export function
|
|
31
|
+
export function isPathSafe(pathname: string, baseDir: string): boolean {
|
|
32
|
+
const decoded = decodeURIComponent(pathname);
|
|
33
|
+
const resolved = resolve(baseDir, decoded.slice(1));
|
|
34
|
+
const baseDirSlash = baseDir.endsWith("/") ? baseDir : baseDir + "/";
|
|
35
|
+
return resolved === baseDir || resolved.startsWith(baseDirSlash);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSlugSafe(slug: string, docsDir: string): boolean {
|
|
39
|
+
const resolved = resolve(docsDir, slug);
|
|
40
|
+
const docsDirSlash = docsDir.endsWith("/") ? docsDir : docsDir + "/";
|
|
41
|
+
return resolved === docsDir || resolved.startsWith(docsDirSlash);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function injectNonce(html: string, nonce: string): string {
|
|
45
|
+
return html.replace(/<script\b(?![^>]*\bsrc\s*=)([^>]*)>/gi, (match) => {
|
|
46
|
+
if (/nonce\s*=/i.test(match)) return match;
|
|
47
|
+
return match.replace(/>$/, ` nonce="${nonce}">`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function htmlResponse(
|
|
52
|
+
html: string,
|
|
53
|
+
nonce: string,
|
|
54
|
+
status = 200,
|
|
55
|
+
allowEval = false
|
|
56
|
+
): Response {
|
|
27
57
|
return new Response(html, {
|
|
28
58
|
status,
|
|
29
59
|
headers: {
|
|
30
60
|
"Content-Type": "text/html",
|
|
31
61
|
...SECURITY_HEADERS,
|
|
32
|
-
"Content-Security-Policy": cspHeader(nonce),
|
|
62
|
+
"Content-Security-Policy": cspHeader(nonce, allowEval),
|
|
33
63
|
},
|
|
34
64
|
});
|
|
35
65
|
}
|
package/.docu/node/server.ts
CHANGED
|
@@ -11,11 +11,11 @@ import DocsPage from "../pages/docs/[[...slug]]";
|
|
|
11
11
|
import NotFoundPage from "../pages/404";
|
|
12
12
|
import IndexPage from "../pages/index";
|
|
13
13
|
import { DocsLayout } from "../components/DocsLayout";
|
|
14
|
-
import { buildClientBundle } from "./hydrate";
|
|
14
|
+
import { buildClientBundle, computeInlineThemeCss } from "./hydrate";
|
|
15
15
|
import { generateSearchIndex } from "./search-indexer";
|
|
16
16
|
import { logger } from "./logger";
|
|
17
17
|
import { initSentry, captureException } from "./sentry";
|
|
18
|
-
import { SECURITY_HEADERS, generateNonce, htmlResponse } from "./security";
|
|
18
|
+
import { SECURITY_HEADERS, generateNonce, isPathSafe, isSlugSafe, htmlResponse } from "./security";
|
|
19
19
|
import { htmlShell as createHtmlShell, hmrScript } from "./html";
|
|
20
20
|
|
|
21
21
|
const docuConfig = loadDocuConfig();
|
|
@@ -31,6 +31,8 @@ let t = performance.now();
|
|
|
31
31
|
const assetManifest = await buildClientBundle();
|
|
32
32
|
logger.bundleDone(Math.round(performance.now() - t));
|
|
33
33
|
|
|
34
|
+
const inlineThemeCss = computeInlineThemeCss();
|
|
35
|
+
|
|
34
36
|
logger.indexStart();
|
|
35
37
|
t = performance.now();
|
|
36
38
|
const records = await generateSearchIndex();
|
|
@@ -91,13 +93,13 @@ function createHtmlResponse(
|
|
|
91
93
|
js: assetManifest.js,
|
|
92
94
|
nonce,
|
|
93
95
|
extraScripts: hmrScript(nonce),
|
|
96
|
+
themeCss: inlineThemeCss,
|
|
94
97
|
});
|
|
95
|
-
return htmlResponse(html, nonce, status);
|
|
98
|
+
return htmlResponse(html, nonce, status, process.env.NODE_ENV !== "production");
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
async function getDocsForSlug(slug: string) {
|
|
99
|
-
|
|
100
|
-
if (!resolved.startsWith(DOCS_DIR)) return null;
|
|
102
|
+
if (!isSlugSafe(slug, DOCS_DIR)) return null;
|
|
101
103
|
|
|
102
104
|
const paths = [
|
|
103
105
|
join(DOCS_DIR, slug, "index.mdx"),
|
|
@@ -215,9 +217,9 @@ function handleIndex(): Response {
|
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
function serveStatic(pathname: string): Response | null {
|
|
220
|
+
if (!isPathSafe(pathname, DIST_DIR)) return null;
|
|
218
221
|
const decoded = decodeURIComponent(pathname);
|
|
219
222
|
const assetPath = resolve(DIST_DIR, decoded.slice(1));
|
|
220
|
-
if (!assetPath.startsWith(DIST_DIR)) return null;
|
|
221
223
|
try {
|
|
222
224
|
const s = statSync(assetPath);
|
|
223
225
|
if (s.isFile()) {
|
|
@@ -231,7 +233,9 @@ function serveStatic(pathname: string): Response | null {
|
|
|
231
233
|
|
|
232
234
|
if (decoded.startsWith("/docs/assets/")) {
|
|
233
235
|
const docsAsset = resolve(DOCS_DIR, "assets", decoded.replace("/docs/assets/", ""));
|
|
234
|
-
|
|
236
|
+
const docsAssetsDir = resolve(DOCS_DIR, "assets");
|
|
237
|
+
const docsAssetsDirSlash = docsAssetsDir.endsWith("/") ? docsAssetsDir : docsAssetsDir + "/";
|
|
238
|
+
if (docsAsset !== docsAssetsDir && !docsAsset.startsWith(docsAssetsDirSlash)) return null;
|
|
235
239
|
try {
|
|
236
240
|
const s = statSync(docsAsset);
|
|
237
241
|
if (s.isFile()) {
|
package/.docu/node/types.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface HomeConfig {
|
|
|
66
66
|
features?: HomeFeature[];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
import type { ThemeConfig } from "@docubook/themes-colors";
|
|
70
|
+
|
|
69
71
|
export interface DocuConfig {
|
|
70
72
|
meta: DocuMeta;
|
|
71
73
|
home?: HomeConfig;
|
|
@@ -73,6 +75,9 @@ export interface DocuConfig {
|
|
|
73
75
|
footer: DocuFooter;
|
|
74
76
|
repo: RepoConfig;
|
|
75
77
|
routes: DocuRoute[];
|
|
78
|
+
themes?: {
|
|
79
|
+
colors: ThemeConfig;
|
|
80
|
+
};
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export interface BuildCache {
|
package/README.md
CHANGED
|
@@ -124,6 +124,94 @@ The `home` section configures your landing page with a hero section and feature
|
|
|
124
124
|
| `hero.actions` | Array of CTA buttons with `text`, `link`, `theme`, `icon` |
|
|
125
125
|
| `features` | Array of feature cards with `icon`, `title`, `description`, `link` |
|
|
126
126
|
|
|
127
|
+
### Theme Colors
|
|
128
|
+
|
|
129
|
+
Flame uses `@docubook/themes-colors` for its config-driven color system. Configure via `docu.json`:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"themes": {
|
|
134
|
+
"colors": "default"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Preset Themes
|
|
140
|
+
|
|
141
|
+
Use a preset name as a string. Three built-in presets are available:
|
|
142
|
+
|
|
143
|
+
| Name | Description | Hue |
|
|
144
|
+
| ------------- | ----------------- | ------ |
|
|
145
|
+
| `"default"` | Modern Blue theme | ~210 |
|
|
146
|
+
| `"freshlime"` | Fresh Lime theme | ~85 |
|
|
147
|
+
| `"coffee"` | Rich Coffee theme | ~25–35 |
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"themes": {
|
|
152
|
+
"colors": "freshlime"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Custom Hex Colors
|
|
158
|
+
|
|
159
|
+
Define a custom primary color as a hex value. The full 24-variable palette is auto-generated from it:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"themes": {
|
|
164
|
+
"colors": {
|
|
165
|
+
"primary": "#FF5733"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Property | Type | Required | Description |
|
|
172
|
+
| --------- | -------------- | -------- | ----------------------------------------------------------------- |
|
|
173
|
+
| `primary` | `string` (hex) | ✅ Yes | Primary brand color. A full light + dark scale is auto-generated. |
|
|
174
|
+
|
|
175
|
+
##### What gets auto-generated from `primary`
|
|
176
|
+
|
|
177
|
+
A single hex color generates **24 CSS variables × 2 modes** (light `:root` + dark `.dark`) plus **12 syntax highlighting tokens × 2 modes** — all derived from the primary color.
|
|
178
|
+
|
|
179
|
+
| Token | Description |
|
|
180
|
+
| -------------------------------------------- | -------------------------- |
|
|
181
|
+
| `--background` / `--foreground` | Page background & text |
|
|
182
|
+
| `--card` / `--card-foreground` | Card surface & text |
|
|
183
|
+
| `--popover` / `--popover-foreground` | Popover surface & text |
|
|
184
|
+
| `--primary` / `--primary-foreground` | Primary brand color & text |
|
|
185
|
+
| `--secondary` / `--secondary-foreground` | Secondary color & text |
|
|
186
|
+
| `--muted` / `--muted-foreground` | Muted surface & text |
|
|
187
|
+
| `--accent` / `--accent-foreground` | Accent color & text |
|
|
188
|
+
| `--destructive` / `--destructive-foreground` | Destructive action & text |
|
|
189
|
+
| `--border` | Border color |
|
|
190
|
+
| `--input` | Input field border |
|
|
191
|
+
| `--ring` | Focus ring color |
|
|
192
|
+
| `--radius` | Border radius value |
|
|
193
|
+
| `--base-100` / `--base-200` / `--base-300` | DaisyUI surface layers |
|
|
194
|
+
| `--base-content` | DaisyUI content color |
|
|
195
|
+
|
|
196
|
+
#### CLI Override
|
|
197
|
+
|
|
198
|
+
Override any theme without editing `docu.json`:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
flame dev --theme freshlime
|
|
202
|
+
flame build --theme coffee
|
|
203
|
+
flame preview --theme default
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Priority
|
|
207
|
+
|
|
208
|
+
Theme resolution follows this order (first match wins):
|
|
209
|
+
1. `--theme` CLI flag (e.g., `flame dev --theme coffee`)
|
|
210
|
+
2. `docu.json` → `themes.colors`
|
|
211
|
+
3. Falls back to `default` preset
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
127
215
|
### Routes
|
|
128
216
|
|
|
129
217
|
When `routes` is an empty array `[]`, Flame automatically scans your `docs/` folder at build-time and generates the sidebar navigation from the directory structure. Folders become collapsible sections, and `.mdx`/`.md` files become links — sorted alphabetically.
|
|
@@ -249,6 +337,11 @@ Copy `.env.example` to `.env` to customize:
|
|
|
249
337
|
# Server port (default: 3000)
|
|
250
338
|
PORT=3000
|
|
251
339
|
|
|
340
|
+
# Theme preset (optional — overrides docu.json themes.colors)
|
|
341
|
+
# FLAME_THEME=default
|
|
342
|
+
# FLAME_THEME=freshlime
|
|
343
|
+
# FLAME_THEME=coffee
|
|
344
|
+
|
|
252
345
|
# Error Monitoring (optional)
|
|
253
346
|
SENTRY_DSN=https://your-dsn@sentry.io/project-id
|
|
254
347
|
```
|
package/bin/cli.js
CHANGED
|
@@ -16,6 +16,12 @@ const COMMAND_MAP = {
|
|
|
16
16
|
|
|
17
17
|
const command = process.argv[2];
|
|
18
18
|
|
|
19
|
+
// Parse --theme flag: set env before importing build script
|
|
20
|
+
const themeIndex = process.argv.indexOf("--theme");
|
|
21
|
+
if (themeIndex !== -1 && themeIndex + 1 < process.argv.length) {
|
|
22
|
+
process.env.FLAME_THEME = process.argv[themeIndex + 1];
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
if (!command || command === "--help" || command === "-h") {
|
|
20
26
|
console.log(`
|
|
21
27
|
@docubook/flame — A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.
|
|
@@ -31,7 +37,8 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
31
37
|
init Scaffold a new project in current directory
|
|
32
38
|
|
|
33
39
|
Options:
|
|
34
|
-
--help
|
|
40
|
+
--help Show this help message
|
|
41
|
+
--theme <name> Override theme preset (e.g. freshlime, coffee). Works with dev, build, preview.
|
|
35
42
|
`);
|
|
36
43
|
process.exit(0);
|
|
37
44
|
}
|
package/docu.schema.json
CHANGED
|
@@ -117,6 +117,35 @@
|
|
|
117
117
|
"type": "array",
|
|
118
118
|
"description": "Documentation navigation routes. Leave empty for auto-detection from docs/ folder.",
|
|
119
119
|
"items": { "$ref": "#/$defs/route" }
|
|
120
|
+
},
|
|
121
|
+
"themes": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"description": "Color themes configuration. Use preset name or custom hex values.",
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"properties": {
|
|
126
|
+
"colors": {
|
|
127
|
+
"description": "Theme colors — preset name (e.g. \"default\", \"freshlime\", \"coffee\") or custom hex object",
|
|
128
|
+
"oneOf": [
|
|
129
|
+
{
|
|
130
|
+
"type": "string",
|
|
131
|
+
"enum": ["default", "freshlime", "coffee"]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"properties": {
|
|
137
|
+
"primary": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
|
|
140
|
+
"description": "Primary brand color in hex format"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"required": ["primary"]
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"required": ["colors"]
|
|
120
149
|
}
|
|
121
150
|
},
|
|
122
151
|
"$defs": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
"lucide-react": "^1.14.0",
|
|
52
52
|
"react": "^19.0.0",
|
|
53
53
|
"react-dom": "^19.0.0",
|
|
54
|
-
"@docubook/ui-react": "^0.1.3",
|
|
55
54
|
"@docubook/mdx-content": "^3.2.1",
|
|
56
|
-
"@docubook/core": "^1.7.0"
|
|
55
|
+
"@docubook/core": "^1.7.0",
|
|
56
|
+
"@docubook/themes-colors": "^0.10.1",
|
|
57
|
+
"@docubook/ui-react": "^0.1.3"
|
|
57
58
|
},
|
|
58
59
|
"peerDependencies": {
|
|
59
60
|
"@sentry/bun": "^9.0.0"
|
package/template/.env.example
CHANGED