@docubook/flame 1.0.1 → 1.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.
@@ -4,8 +4,8 @@ import { useState } from "react";
4
4
  import { cn } from "../node/utils";
5
5
  import { Dropdown, DropdownItem } from "@docubook/ui-react/dropdown";
6
6
  import { routes } from "../node/client-routes";
7
- import { ChevronsUpDown, Check, type LucideIcon } from "lucide-react";
8
- import * as LucideIcons from "lucide-react";
7
+ import { ChevronsUpDown, Check } from "lucide-react";
8
+ import { renderLucideIcon } from "./Lucide";
9
9
 
10
10
  interface ContextProps {
11
11
  className?: string;
@@ -24,11 +24,6 @@ function getActiveContextRoute(path: string) {
24
24
  return getContextRoutes().find((route) => docPath.startsWith(route.href));
25
25
  }
26
26
 
27
- function getIcon(name: string): LucideIcon {
28
- const Icon = LucideIcons[name as keyof typeof LucideIcons] as LucideIcon | undefined;
29
- return Icon || LucideIcons.FileQuestion;
30
- }
31
-
32
27
  export function Context({ className }: ContextProps) {
33
28
  const [pathname] = useState(() =>
34
29
  typeof window !== "undefined" ? window.location.pathname : "/"
@@ -57,10 +52,7 @@ export function Context({ className }: ContextProps) {
57
52
  <div className="flex min-w-0 items-center gap-2">
58
53
  {displayRoute?.context?.icon && (
59
54
  <span className="text-primary bg-primary/10 border-primary flex-shrink-0 rounded border p-0.5">
60
- {(() => {
61
- const Icon = getIcon(displayRoute.context.icon);
62
- return <Icon className="h-4 w-4" />;
63
- })()}
55
+ {renderLucideIcon(displayRoute.context.icon, "h-4 w-4")}
64
56
  </span>
65
57
  )}
66
58
  <span className="truncate">{displayRoute?.context?.title || displayRoute?.title}</span>
@@ -93,10 +85,7 @@ export function Context({ className }: ContextProps) {
93
85
  isActive ? "text-primary" : "text-base-content/60"
94
86
  )}
95
87
  >
96
- {(() => {
97
- const Icon = getIcon(route.context.icon);
98
- return <Icon className="h-4 w-4" />;
99
- })()}
88
+ {renderLucideIcon(route.context.icon)}
100
89
  </span>
101
90
  )}
102
91
  <div className="min-w-0 flex-1">
@@ -0,0 +1,20 @@
1
+ import * as LucideIcons from "lucide-react";
2
+ import type { LucideIcon } from "lucide-react";
3
+
4
+ /**
5
+ * Get Lucide icon component by name
6
+ * Icon names must match Lucide's export names exactly (e.g., "Zap", "BookOpen", "Search")
7
+ */
8
+ export function getLucideIcon(name: string): LucideIcon | null {
9
+ if (!name) return null;
10
+ const icon = (LucideIcons as Record<string, LucideIcon | undefined>)[name];
11
+ return icon || null;
12
+ }
13
+
14
+ /**
15
+ * Render Lucide icon component
16
+ */
17
+ export function renderLucideIcon(name: string, className?: string) {
18
+ const Icon = getLucideIcon(name);
19
+ return Icon ? <Icon className={className} /> : null;
20
+ }
@@ -1,9 +1,40 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback } from "react";
4
- import { Search as SearchIcon, FileText, CornerDownLeft, Hash, AlignLeft } from "lucide-react";
4
+ import {
5
+ Search as SearchIcon,
6
+ FileText,
7
+ CornerDownLeft,
8
+ Hash,
9
+ AlignLeft,
10
+ Command,
11
+ Option,
12
+ ChevronUp,
13
+ ArrowBigUp,
14
+ CircleArrowOutUpLeft,
15
+ Space,
16
+ Delete,
17
+ ArrowRightToLine,
18
+ ChevronDown,
19
+ ChevronLeft,
20
+ ChevronRight,
21
+ } from "lucide-react";
5
22
  import { Modal, useModal } from "@docubook/ui-react/modal";
6
23
  import { Kbd, FnKey } from "@docubook/ui-react/kbd";
24
+
25
+ FnKey.configure({
26
+ Command,
27
+ Option,
28
+ ChevronUp,
29
+ ArrowBigUp,
30
+ CircleArrowOutUpLeft,
31
+ Space,
32
+ Delete,
33
+ ArrowRightToLine,
34
+ ChevronDown,
35
+ ChevronLeft,
36
+ ChevronRight,
37
+ });
7
38
  import { cn } from "../node/utils";
8
39
  import { search, type SearchResult } from "../node/search";
9
40
  import type { SearchRecord } from "../node/search-indexer";
@@ -181,16 +212,16 @@ export default function Search({ className }: SearchProps) {
181
212
 
182
213
  <div className="border-base-300 hidden items-center gap-3 border-t px-4 py-2.5 md:flex">
183
214
  <div className="flex items-center gap-1">
184
- <Kbd size="xs">
215
+ <Kbd size="s">
185
216
  <FnKey.Down />
186
217
  </Kbd>
187
- <Kbd size="xs">
218
+ <Kbd size="s">
188
219
  <FnKey.Up />
189
220
  </Kbd>
190
221
  <span className="text-base-content/50 text-xs">navigate</span>
191
222
  </div>
192
223
  <div className="flex items-center gap-1">
193
- <Kbd size="xs">
224
+ <Kbd size="s">
194
225
  <CornerDownLeft className="h-3 w-3" />
195
226
  </Kbd>
196
227
  <span className="text-base-content/50 text-xs">select</span>
@@ -79,7 +79,7 @@ const iconMap: Record<string, ReturnType<typeof createIcon>> = {
79
79
  mastodon: MastodonIcon,
80
80
  };
81
81
 
82
- function getIcon(name: string) {
82
+ export function getSocialIcon(name: string) {
83
83
  const key = name.toLowerCase().replace(/\s+/g, "");
84
84
  return iconMap[key] || null;
85
85
  }
@@ -92,7 +92,7 @@ export default function Social({ className = "", size = 16 }: SocialProps) {
92
92
  return (
93
93
  <div className={`flex gap-1 ${className}`}>
94
94
  {socialLinks.map((link) => {
95
- const Icon = getIcon(link.name);
95
+ const Icon = getSocialIcon(link.name);
96
96
  return (
97
97
  <a
98
98
  key={link.name}
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import type { HomeFeature } from "../../node/types";
4
+ import { cn } from "../../node/utils";
5
+ import { renderLucideIcon } from "../Lucide";
6
+
7
+ interface FeaturesProps {
8
+ features: HomeFeature[];
9
+ className?: string;
10
+ }
11
+
12
+ interface FeatureCardProps {
13
+ feature: HomeFeature;
14
+ index: number;
15
+ }
16
+
17
+ function FeatureCard({ feature, index }: FeatureCardProps) {
18
+ const Wrapper = feature.link ? "a" : "div";
19
+ const wrapperProps = feature.link ? { href: feature.link } : {};
20
+
21
+ // Use index-based patternId to avoid collisions
22
+ const patternId = `grid-${index}`;
23
+
24
+ return (
25
+ <Wrapper
26
+ {...wrapperProps}
27
+ className={cn(
28
+ "border-base-200 bg-base-100 hover:border-primary/40 group relative overflow-hidden rounded-2xl border p-6 transition-all hover:shadow-lg",
29
+ feature.link && "cursor-pointer"
30
+ )}
31
+ >
32
+ {/* Grid pattern background */}
33
+ <svg
34
+ className="absolute inset-0 h-full w-full"
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ style={{ color: "var(--color-primary)" }}
37
+ aria-hidden="true"
38
+ >
39
+ <defs>
40
+ <pattern id={patternId} width="40" height="40" patternUnits="userSpaceOnUse">
41
+ <path
42
+ d="M 40 0 L 0 0 0 40"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ strokeWidth="0.5"
46
+ opacity="0.15"
47
+ />
48
+ </pattern>
49
+ </defs>
50
+ <rect width="100%" height="100%" fill={`url(#${patternId})`} />
51
+ </svg>
52
+
53
+ {/* Content */}
54
+ <div className="relative z-10">
55
+ {feature.icon && (
56
+ <div className="bg-primary/10 mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg">
57
+ {renderLucideIcon(feature.icon, "h-6 w-6 text-primary")}
58
+ </div>
59
+ )}
60
+ <h3 className="mb-2 text-lg font-semibold">{feature.title}</h3>
61
+ <p className="text-muted-foreground text-sm">{feature.description}</p>
62
+ </div>
63
+ </Wrapper>
64
+ );
65
+ }
66
+
67
+ export function Features({ features, className }: FeaturesProps) {
68
+ if (!features || features.length === 0) return null;
69
+
70
+ return (
71
+ <div className={cn("mx-auto max-w-5xl px-6 pb-24", className)}>
72
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
73
+ {features.map((feature, index) => (
74
+ <FeatureCard key={index} feature={feature} index={index} />
75
+ ))}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import type { Hero as HeroType, HeroAction } from "../../node/types";
4
+ import { cn } from "../../node/utils";
5
+ import { renderLucideIcon } from "../Lucide";
6
+ import { getSocialIcon } from "../Social";
7
+
8
+ interface HeroProps {
9
+ hero: HeroType;
10
+ className?: string;
11
+ }
12
+
13
+ interface ActionButtonProps {
14
+ action: HeroAction;
15
+ }
16
+
17
+ function isExternalLink(link: string): boolean {
18
+ return /^https?:\/\//.test(link);
19
+ }
20
+
21
+ function renderActionButtonIcon(iconName: string | undefined, className?: string) {
22
+ if (!iconName) return null;
23
+
24
+ // Try Lucide icon first
25
+ const lucideIcon = renderLucideIcon(iconName, className);
26
+ if (lucideIcon) return lucideIcon;
27
+
28
+ // Fallback to social icon
29
+ const SocialIcon = getSocialIcon(iconName);
30
+ if (SocialIcon) return <SocialIcon className={className} />;
31
+
32
+ return null;
33
+ }
34
+
35
+ function ActionButton({ action }: ActionButtonProps) {
36
+ const themeClasses = {
37
+ primary: "bg-primary text-primary-content hover:bg-primary/90",
38
+ secondary: "bg-secondary text-secondary-content hover:bg-secondary/90",
39
+ ghost: "bg-transparent text-muted-foreground border border-base-300 hover:bg-base-200",
40
+ };
41
+
42
+ const isExternal = isExternalLink(action.link);
43
+
44
+ return (
45
+ <a
46
+ href={action.link}
47
+ target={isExternal ? "_blank" : undefined}
48
+ rel={isExternal ? "noopener noreferrer" : undefined}
49
+ className={cn(
50
+ "inline-flex items-center gap-2 rounded-lg px-6 py-3 text-sm font-medium transition-colors",
51
+ themeClasses[action.theme || "primary"]
52
+ )}
53
+ >
54
+ {renderActionButtonIcon(action.icon, "h-4 w-4")}
55
+ {action.text}
56
+ </a>
57
+ );
58
+ }
59
+
60
+ export function Hero({ hero, className }: HeroProps) {
61
+ const { tagline, headline, description, actions } = hero;
62
+
63
+ return (
64
+ <div className={cn("mx-auto max-w-4xl px-6 py-32 sm:py-44", className)}>
65
+ <div className="text-center">
66
+ {tagline && <p className="text-primary mb-4 text-lg font-semibold">{tagline}</p>}
67
+ <h1 className="text-balance text-5xl font-semibold tracking-tight sm:text-7xl">
68
+ {headline}
69
+ </h1>
70
+ {description && (
71
+ <p className="text-muted-foreground mt-8 text-pretty text-lg sm:text-xl">{description}</p>
72
+ )}
73
+ {actions && actions.length > 0 && (
74
+ <div className="mt-10 flex flex-wrap items-center justify-center gap-4">
75
+ {actions.map((action, index) => (
76
+ <ActionButton key={index} action={action} />
77
+ ))}
78
+ </div>
79
+ )}
80
+ </div>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,2 @@
1
+ export { Hero } from "./Hero";
2
+ export { Features } from "./Features";
@@ -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);
@@ -11,7 +11,7 @@ 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";
@@ -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,6 +93,7 @@ function createHtmlResponse(
91
93
  js: assetManifest.js,
92
94
  nonce,
93
95
  extraScripts: hmrScript(nonce),
96
+ themeCss: inlineThemeCss,
94
97
  });
95
98
  return htmlResponse(html, nonce, status);
96
99
  }
@@ -40,12 +40,44 @@ export interface RepoConfig {
40
40
  edit: boolean;
41
41
  }
42
42
 
43
+ export interface HeroAction {
44
+ text: string;
45
+ link: string;
46
+ theme?: "primary" | "secondary" | "ghost";
47
+ icon?: string;
48
+ }
49
+
50
+ export interface Hero {
51
+ tagline?: string;
52
+ headline: string;
53
+ description?: string;
54
+ actions?: HeroAction[];
55
+ }
56
+
57
+ export interface HomeFeature {
58
+ icon?: string;
59
+ title: string;
60
+ description: string;
61
+ link?: string;
62
+ }
63
+
64
+ export interface HomeConfig {
65
+ hero?: Hero;
66
+ features?: HomeFeature[];
67
+ }
68
+
69
+ import type { ThemeConfig } from "@docubook/themes-colors";
70
+
43
71
  export interface DocuConfig {
44
72
  meta: DocuMeta;
73
+ home?: HomeConfig;
45
74
  navbar: DocuNavbar;
46
75
  footer: DocuFooter;
47
76
  repo: RepoConfig;
48
77
  routes: DocuRoute[];
78
+ themes?: {
79
+ colors: ThemeConfig;
80
+ };
49
81
  }
50
82
 
51
83
  export interface BuildCache {
@@ -1,5 +1,6 @@
1
- import * as icons from "lucide-react";
2
1
  import { loadDocuConfig } from "../node/paths";
2
+ import { Hero, Features } from "../components/home";
3
+ import type { HomeFeature } from "../node/types";
3
4
 
4
5
  const docuConfig = loadDocuConfig();
5
6
 
@@ -17,9 +18,26 @@ interface RouteItem {
17
18
  }
18
19
 
19
20
  export default function IndexPage() {
20
- const { meta } = docuConfig;
21
+ const { meta, home } = docuConfig;
21
22
  const routes = (docuConfig.routes as RouteItem[]) || [];
22
- const cards = routes.filter((r) => r.context);
23
+
24
+ // Use home.features if configured, otherwise fallback to routes with context
25
+ const features: HomeFeature[] =
26
+ home?.features ||
27
+ routes
28
+ .filter((r) => r.context)
29
+ .map((route) => ({
30
+ icon: route.context?.icon,
31
+ title: route.context?.title || route.title,
32
+ description: route.context?.description || "",
33
+ link: `/docs${route.href}${route.items?.[0]?.href || ""}`,
34
+ }));
35
+
36
+ // Use home.hero if configured, otherwise fallback to meta
37
+ const hero = home?.hero || {
38
+ headline: meta.title,
39
+ description: meta.description,
40
+ };
23
41
 
24
42
  return (
25
43
  <div className="bg-base-100 relative isolate min-h-screen overflow-hidden">
@@ -39,70 +57,11 @@ export default function IndexPage() {
39
57
  />
40
58
  </div>
41
59
 
42
- {/* Hero */}
43
- <div className="mx-auto max-w-2xl py-32 sm:py-44">
44
- <div className="text-center">
45
- <h1 className="text-balance text-5xl font-semibold tracking-tight sm:text-7xl">
46
- {meta.title}
47
- </h1>
48
- <p className="text-muted-foreground mt-8 text-pretty text-lg sm:text-xl">
49
- {meta.description}
50
- </p>
51
- </div>
52
- </div>
60
+ {/* Hero Section */}
61
+ <Hero hero={hero} />
53
62
 
54
- {/* Showcase Cards */}
55
- {cards.length > 0 && (
56
- <div className="relative mx-auto max-w-4xl px-6 pb-24">
57
- <div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
58
- {cards.map((route) => {
59
- const Icon = (icons as unknown as Record<string, icons.LucideIcon>)[
60
- route.context!.icon || ""
61
- ];
62
- return (
63
- <a
64
- key={route.href}
65
- href={`/docs${route.href}${route.items?.[0]?.href || ""}`}
66
- className="group"
67
- >
68
- <div className="border-primary/20 bg-primary/5 group-hover:border-primary/40 relative flex aspect-[16/9] items-center justify-center overflow-hidden rounded-2xl border transition">
69
- {/* Grid pattern */}
70
- <svg
71
- className="absolute inset-0 h-full w-full"
72
- xmlns="http://www.w3.org/2000/svg"
73
- >
74
- <defs>
75
- <pattern
76
- id={`grid-${route.href}`}
77
- width="40"
78
- height="40"
79
- patternUnits="userSpaceOnUse"
80
- >
81
- <path
82
- d="M 40 0 L 0 0 0 40"
83
- fill="none"
84
- stroke="currentColor"
85
- strokeWidth="0.5"
86
- className="text-primary/15"
87
- />
88
- </pattern>
89
- </defs>
90
- <rect width="100%" height="100%" fill={`url(#grid-${route.href})`} />
91
- </svg>
92
- {Icon && (
93
- <Icon className="text-primary relative z-10" size={80} strokeWidth={1} />
94
- )}
95
- </div>
96
- <h3 className="mt-4 text-lg font-semibold">
97
- {route.context!.title || route.title}
98
- </h3>
99
- <p className="text-muted-foreground text-m mt-1">{route.context!.description}</p>
100
- </a>
101
- );
102
- })}
103
- </div>
104
- </div>
105
- )}
63
+ {/* Features Section */}
64
+ <Features features={features} />
106
65
 
107
66
  {/* Bottom gradient blob */}
108
67
  <div
package/README.md CHANGED
@@ -65,25 +65,153 @@ bun run deploy # Build + prepare for GitHub Pages
65
65
 
66
66
  ## Configuration
67
67
 
68
- `docu.json` controls your site:
68
+ `docu.json` controls your site. Full example:
69
69
 
70
70
  ```json
71
71
  {
72
+ "$schema": "https://cdn.jsdelivr.net/npm/@docubook/flame/docu.schema.json",
72
73
  "meta": {
73
74
  "title": "My Docs",
74
- "description": "Documentation powered by DocuBook Flame"
75
+ "description": "Documentation powered by DocuBook Flame",
76
+ "baseURL": "https://example.com",
77
+ "favicon": "/docs/assets/images/favicon.ico"
78
+ },
79
+ "home": {
80
+ "hero": {
81
+ "tagline": "#MyDocs",
82
+ "headline": "Documentation",
83
+ "description": "Welcome to your documentation site.",
84
+ "actions": [
85
+ { "text": "Get Started", "link": "/docs", "theme": "primary" },
86
+ { "text": "GitHub", "link": "https://github.com", "theme": "ghost", "icon": "github" }
87
+ ]
88
+ },
89
+ "features": [
90
+ { "icon": "Zap", "title": "Fast", "description": "Instant builds.", "link": "/docs" }
91
+ ]
75
92
  },
76
93
  "navbar": {
94
+ "logo": { "src": "/docs/assets/logo.svg", "alt": "Logo" },
77
95
  "logoText": "My Docs",
78
96
  "menu": [
79
97
  { "title": "Home", "href": "/" },
80
98
  { "title": "Docs", "href": "/docs" }
81
99
  ]
82
100
  },
101
+ "footer": {
102
+ "social": [
103
+ { "name": "github", "url": "https://github.com/you" }
104
+ ]
105
+ },
106
+ "repo": {
107
+ "url": "https://github.com/you/repo",
108
+ "path": "blob/main/{filePath}",
109
+ "edit": true
110
+ },
83
111
  "routes": []
84
112
  }
85
113
  ```
86
114
 
115
+ ### Home Page
116
+
117
+ The `home` section configures your landing page with a hero section and feature cards:
118
+
119
+ | Property | Description |
120
+ | ------------------ | ------------------------------------------------------------------- |
121
+ | `hero.tagline` | Small text above the headline (e.g., product name) |
122
+ | `hero.headline` | **Required.** Main heading text |
123
+ | `hero.description` | Description below the headline |
124
+ | `hero.actions` | Array of CTA buttons with `text`, `link`, `theme`, `icon` |
125
+ | `features` | Array of feature cards with `icon`, `title`, `description`, `link` |
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
+
87
215
  ### Routes
88
216
 
89
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.
@@ -166,11 +294,11 @@ Reference in MDX:
166
294
 
167
295
  ## Comparison
168
296
 
169
- | Framework | Runtime | UI | Approach |
170
- |---|---|---|---|
171
- | Docusaurus | Node.js | React | Full-featured, plugin-heavy |
172
- | VitePress | Node.js | Vue | Lightweight, Vue-only |
173
- | Nextra | Node.js | React | Next.js-based |
297
+ | Framework | Runtime | UI | Approach |
298
+ | ------------------- | ------- | --------- | --------------------------- |
299
+ | Docusaurus | Node.js | React | Full-featured, plugin-heavy |
300
+ | VitePress | Node.js | Vue | Lightweight, Vue-only |
301
+ | Nextra | Node.js | React | Next.js-based |
174
302
  | **@docubook/flame** | **Bun** | **React** | **Minimal, Bun-native SSR** |
175
303
 
176
304
  ---
@@ -209,6 +337,11 @@ Copy `.env.example` to `.env` to customize:
209
337
  # Server port (default: 3000)
210
338
  PORT=3000
211
339
 
340
+ # Theme preset (optional — overrides docu.json themes.colors)
341
+ # FLAME_THEME=default
342
+ # FLAME_THEME=freshlime
343
+ # FLAME_THEME=coffee
344
+
212
345
  # Error Monitoring (optional)
213
346
  SENTRY_DSN=https://your-dsn@sentry.io/project-id
214
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
@@ -10,6 +10,7 @@
10
10
  "meta": {
11
11
  "type": "object",
12
12
  "description": "Site metadata",
13
+ "additionalProperties": false,
13
14
  "properties": {
14
15
  "title": { "type": "string", "description": "Site title" },
15
16
  "description": { "type": "string", "description": "Site description" },
@@ -18,12 +19,27 @@
18
19
  },
19
20
  "required": ["title"]
20
21
  },
22
+ "home": {
23
+ "type": "object",
24
+ "description": "Home page configuration",
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "hero": { "$ref": "#/$defs/hero" },
28
+ "features": {
29
+ "type": "array",
30
+ "description": "Feature cards displayed on home page",
31
+ "items": { "$ref": "#/$defs/homeFeature" }
32
+ }
33
+ }
34
+ },
21
35
  "navbar": {
22
36
  "type": "object",
23
37
  "description": "Navigation bar configuration",
38
+ "additionalProperties": false,
24
39
  "properties": {
25
40
  "logo": {
26
41
  "type": "object",
42
+ "additionalProperties": false,
27
43
  "properties": {
28
44
  "src": { "type": "string", "description": "Logo image path" },
29
45
  "alt": { "type": "string", "description": "Logo alt text" }
@@ -35,6 +51,7 @@
35
51
  "description": "Top navigation menu items",
36
52
  "items": {
37
53
  "type": "object",
54
+ "additionalProperties": false,
38
55
  "properties": {
39
56
  "title": { "type": "string" },
40
57
  "href": { "type": "string" }
@@ -47,12 +64,14 @@
47
64
  "footer": {
48
65
  "type": "object",
49
66
  "description": "Footer configuration",
67
+ "additionalProperties": false,
50
68
  "properties": {
51
69
  "social": {
52
70
  "type": "array",
53
71
  "description": "Social media links",
54
72
  "items": {
55
73
  "type": "object",
74
+ "additionalProperties": false,
56
75
  "properties": {
57
76
  "name": {
58
77
  "type": "string",
@@ -84,6 +103,7 @@
84
103
  "repo": {
85
104
  "type": "object",
86
105
  "description": "Repository configuration for edit links",
106
+ "additionalProperties": false,
87
107
  "properties": {
88
108
  "url": { "type": "string", "description": "Repository URL" },
89
109
  "path": {
@@ -97,11 +117,41 @@
97
117
  "type": "array",
98
118
  "description": "Documentation navigation routes. Leave empty for auto-detection from docs/ folder.",
99
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"]
100
149
  }
101
150
  },
102
151
  "$defs": {
103
152
  "route": {
104
153
  "type": "object",
154
+ "additionalProperties": false,
105
155
  "properties": {
106
156
  "title": { "type": "string", "description": "Display title" },
107
157
  "href": { "type": "string", "description": "URL path" },
@@ -112,6 +162,7 @@
112
162
  "context": {
113
163
  "type": "object",
114
164
  "description": "Context metadata for sidebar section",
165
+ "additionalProperties": false,
115
166
  "properties": {
116
167
  "icon": { "type": "string", "description": "Lucide icon name" },
117
168
  "title": { "type": "string", "description": "Context switcher title" },
@@ -125,6 +176,56 @@
125
176
  }
126
177
  },
127
178
  "required": ["title", "href"]
179
+ },
180
+ "hero": {
181
+ "type": "object",
182
+ "description": "Hero section configuration for home page",
183
+ "additionalProperties": false,
184
+ "properties": {
185
+ "tagline": {
186
+ "type": "string",
187
+ "description": "Tagline displayed above the headline (e.g., product name)"
188
+ },
189
+ "headline": { "type": "string", "description": "Main heading text" },
190
+ "description": {
191
+ "type": "string",
192
+ "description": "Description text displayed below the headline"
193
+ },
194
+ "actions": {
195
+ "type": "array",
196
+ "description": "Action buttons displayed in the hero section",
197
+ "items": { "$ref": "#/$defs/heroAction" }
198
+ }
199
+ },
200
+ "required": ["headline"]
201
+ },
202
+ "heroAction": {
203
+ "type": "object",
204
+ "description": "Action button in hero section",
205
+ "additionalProperties": false,
206
+ "properties": {
207
+ "text": { "type": "string", "description": "Button label" },
208
+ "link": { "type": "string", "description": "Button link URL" },
209
+ "theme": {
210
+ "type": "string",
211
+ "description": "Button color theme",
212
+ "enum": ["primary", "secondary", "ghost"]
213
+ },
214
+ "icon": { "type": "string", "description": "Lucide icon name" }
215
+ },
216
+ "required": ["text", "link"]
217
+ },
218
+ "homeFeature": {
219
+ "type": "object",
220
+ "description": "Feature card for home page",
221
+ "additionalProperties": false,
222
+ "properties": {
223
+ "icon": { "type": "string", "description": "Lucide icon name" },
224
+ "title": { "type": "string", "description": "Feature title" },
225
+ "description": { "type": "string", "description": "Feature description" },
226
+ "link": { "type": "string", "description": "Feature link URL" }
227
+ },
228
+ "required": ["title", "description"]
128
229
  }
129
230
  }
130
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
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/mdx-content": "^3.2.1",
55
54
  "@docubook/core": "^1.7.0",
56
- "@docubook/ui-react": "^0.1.2"
55
+ "@docubook/mdx-content": "^3.2.1",
56
+ "@docubook/ui-react": "^0.1.3",
57
+ "@docubook/themes-colors": "^0.10.0"
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,49 @@
6
6
  "baseURL": "http://localhost:3000",
7
7
  "favicon": "/docs/assets/images/favicon.ico"
8
8
  },
9
+ "themes": {
10
+ "colors": "default"
11
+ },
12
+ "home": {
13
+ "hero": {
14
+ "tagline": "#MyDocs",
15
+ "headline": "Documentation",
16
+ "description": "Welcome to your documentation site powered by DocuBook Flame.",
17
+ "actions": [
18
+ {
19
+ "text": "Get Started",
20
+ "link": "/docs/getting-started/introduction",
21
+ "theme": "primary"
22
+ },
23
+ {
24
+ "text": "GitHub",
25
+ "link": "https://github.com/gitfromwildan",
26
+ "theme": "ghost",
27
+ "icon": "github"
28
+ }
29
+ ]
30
+ },
31
+ "features": [
32
+ {
33
+ "icon": "Zap",
34
+ "title": "Lightning Fast",
35
+ "description": "Powered by Bun for instant builds and hot module replacement.",
36
+ "link": "/docs"
37
+ },
38
+ {
39
+ "icon": "Paintbrush",
40
+ "title": "Themeable",
41
+ "description": "Customize with Tailwind CSS and DaisyUI for beautiful documentation.",
42
+ "link": "#"
43
+ },
44
+ {
45
+ "icon": "Search",
46
+ "title": "Full-Text Search",
47
+ "description": "Built-in search for easy content discovery.",
48
+ "link": "#"
49
+ }
50
+ ]
51
+ },
9
52
  "navbar": {
10
53
  "logo": {
11
54
  "src": "/docs/assets/images/docu.svg",