@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.
- package/.docu/components/Context.tsx +4 -15
- package/.docu/components/Lucide.tsx +20 -0
- package/.docu/components/Search.tsx +35 -4
- package/.docu/components/Social.tsx +2 -2
- package/.docu/components/home/Features.tsx +79 -0
- package/.docu/components/home/Hero.tsx +83 -0
- package/.docu/components/home/index.tsx +2 -0
- package/.docu/node/build.ts +6 -1
- package/.docu/node/html.ts +4 -2
- package/.docu/node/hydrate.ts +65 -1
- package/.docu/node/server.ts +4 -1
- package/.docu/node/types.ts +32 -0
- package/.docu/pages/index.tsx +25 -66
- package/README.md +140 -7
- package/bin/cli.js +8 -1
- package/docu.schema.json +101 -0
- package/package.json +4 -3
- package/template/.env.example +5 -0
- package/template/docu.json +43 -0
|
@@ -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
|
|
8
|
-
import
|
|
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 {
|
|
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="
|
|
215
|
+
<Kbd size="s">
|
|
185
216
|
<FnKey.Down />
|
|
186
217
|
</Kbd>
|
|
187
|
-
<Kbd size="
|
|
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="
|
|
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
|
|
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 =
|
|
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
|
+
}
|
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/server.ts
CHANGED
|
@@ -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
|
}
|
package/.docu/node/types.ts
CHANGED
|
@@ -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 {
|
package/.docu/pages/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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
|
-
{/*
|
|
55
|
-
{
|
|
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
|
-
|
|
|
170
|
-
|
|
171
|
-
| Docusaurus
|
|
172
|
-
| VitePress
|
|
173
|
-
| Nextra
|
|
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
|
|
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
|
|
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/
|
|
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"
|
package/template/.env.example
CHANGED
package/template/docu.json
CHANGED
|
@@ -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",
|