@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.
@@ -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}`;
@@ -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>
@@ -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
- const cssContent = await Bun.file(tmpCss).arrayBuffer();
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);
@@ -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
- const headers: Record<string, string> = { "Content-Type": contentType };
61
- if (contentType === "text/html") Object.assign(headers, SECURITY_HEADERS);
62
- return new Response(Bun.file(filePath), { headers });
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
- return new Response(Bun.file(notFoundPath), {
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: { "Content-Type": "text/html", ...SECURITY_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
 
@@ -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
- `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'`,
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 htmlResponse(html: string, nonce: string, status = 200): Response {
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
  }
@@ -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
- const resolved = resolve(DOCS_DIR, slug);
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
- if (!docsAsset.startsWith(resolve(DOCS_DIR, "assets"))) return null;
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()) {
@@ -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 Show this help message
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.0",
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"
@@ -1,5 +1,10 @@
1
1
  # Server
2
2
  PORT=3000
3
3
 
4
+ # Theme (optional — overrides docu.json themes.colors)
5
+ # FLAME_THEME=default
6
+ # FLAME_THEME=freshlime
7
+ # FLAME_THEME=coffee
8
+
4
9
  # Error Monitoring (optional)
5
10
  # SENTRY_DSN=https://your-dsn@sentry.io/project-id
@@ -6,6 +6,9 @@
6
6
  "baseURL": "http://localhost:3000",
7
7
  "favicon": "/docs/assets/images/favicon.ico"
8
8
  },
9
+ "themes": {
10
+ "colors": "default"
11
+ },
9
12
  "home": {
10
13
  "hero": {
11
14
  "tagline": "#MyDocs",