@docubook/flame 1.0.0 → 1.1.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/Sidebar.tsx +5 -1
- package/.docu/components/Social.tsx +2 -2
- package/.docu/components/Sublink.tsx +6 -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/search-indexer.ts +2 -2
- package/.docu/node/types.ts +27 -0
- package/.docu/pages/index.tsx +25 -66
- package/README.md +48 -8
- package/docu.schema.json +72 -0
- package/package.json +2 -2
- package/template/docs/index.mdx +4 -4
- package/template/docu.json +40 -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>
|
|
@@ -141,7 +141,11 @@ export function MobileBar({
|
|
|
141
141
|
}
|
|
142
142
|
>
|
|
143
143
|
{(docuConfig.navbar?.menu || []).map((item) => (
|
|
144
|
-
<li
|
|
144
|
+
<li
|
|
145
|
+
key={item.href}
|
|
146
|
+
role="menuitem"
|
|
147
|
+
className="text-base-content/80 hover:text-base-content hover:bg-base-200 cursor-pointer"
|
|
148
|
+
>
|
|
145
149
|
<Anchor
|
|
146
150
|
href={item.href}
|
|
147
151
|
activeWhen={(path: string) =>
|
|
@@ -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}
|
|
@@ -33,6 +33,9 @@ export default function Sublink({
|
|
|
33
33
|
return currentPathname.startsWith(fullHref) && currentPathname !== fullHref;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
// Shared padding based on nesting level
|
|
37
|
+
const levelPadding = cn(level === 1 && "pl-2", level === 2 && "pl-4", level >= 3 && "pl-6");
|
|
38
|
+
|
|
36
39
|
// Leaf node (no children)
|
|
37
40
|
if (!items) {
|
|
38
41
|
const isActive = currentPathname === fullHref || currentPathname === `${fullHref}.html`;
|
|
@@ -52,7 +55,8 @@ export default function Sublink({
|
|
|
52
55
|
<div
|
|
53
56
|
className={cn(
|
|
54
57
|
"py-1.5",
|
|
55
|
-
|
|
58
|
+
levelPadding,
|
|
59
|
+
level >= 2 && "border-l-2",
|
|
56
60
|
level >= 2 && (isActive ? "border-primary" : "border-base-300")
|
|
57
61
|
)}
|
|
58
62
|
>
|
|
@@ -63,7 +67,7 @@ export default function Sublink({
|
|
|
63
67
|
|
|
64
68
|
// Section with children
|
|
65
69
|
return (
|
|
66
|
-
<div className="flex flex-col">
|
|
70
|
+
<div className={cn("flex flex-col", levelPadding)}>
|
|
67
71
|
{/* Section header */}
|
|
68
72
|
<button
|
|
69
73
|
type="button"
|
|
@@ -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
|
+
}
|
|
@@ -56,7 +56,7 @@ function slugify(text: string): string {
|
|
|
56
56
|
.replace(/^-+|-+$/g, "");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function stripJsx(content: string): string {
|
|
59
|
+
export function stripJsx(content: string): string {
|
|
60
60
|
let result = content;
|
|
61
61
|
let prev = "";
|
|
62
62
|
while (result !== prev) {
|
|
@@ -68,7 +68,7 @@ function stripJsx(content: string): string {
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function extractRecords(filePath: string, raw: string): SearchRecord[] {
|
|
71
|
+
export function extractRecords(filePath: string, raw: string): SearchRecord[] {
|
|
72
72
|
const { frontmatter, strippedContent: content } = extractFrontmatterWithContent<Frontmatter>(raw);
|
|
73
73
|
const records: SearchRecord[] = [];
|
|
74
74
|
const url = `/docs/${filePath}`;
|
package/.docu/node/types.ts
CHANGED
|
@@ -40,8 +40,35 @@ 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
|
+
|
|
43
69
|
export interface DocuConfig {
|
|
44
70
|
meta: DocuMeta;
|
|
71
|
+
home?: HomeConfig;
|
|
45
72
|
navbar: DocuNavbar;
|
|
46
73
|
footer: DocuFooter;
|
|
47
74
|
repo: RepoConfig;
|
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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
mkdir my-docs && cd my-docs
|
|
19
|
-
bun add @docubook/flame
|
|
19
|
+
bun add @docubook/flame
|
|
20
20
|
bunx flame init
|
|
21
21
|
bun run dev
|
|
22
22
|
```
|
|
@@ -65,25 +65,65 @@ 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
|
+
|
|
87
127
|
### Routes
|
|
88
128
|
|
|
89
129
|
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 +206,11 @@ Reference in MDX:
|
|
|
166
206
|
|
|
167
207
|
## Comparison
|
|
168
208
|
|
|
169
|
-
|
|
|
170
|
-
|
|
171
|
-
| Docusaurus
|
|
172
|
-
| VitePress
|
|
173
|
-
| Nextra
|
|
209
|
+
| Framework | Runtime | UI | Approach |
|
|
210
|
+
| ------------------- | ------- | --------- | --------------------------- |
|
|
211
|
+
| Docusaurus | Node.js | React | Full-featured, plugin-heavy |
|
|
212
|
+
| VitePress | Node.js | Vue | Lightweight, Vue-only |
|
|
213
|
+
| Nextra | Node.js | React | Next.js-based |
|
|
174
214
|
| **@docubook/flame** | **Bun** | **React** | **Minimal, Bun-native SSR** |
|
|
175
215
|
|
|
176
216
|
---
|
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": {
|
|
@@ -102,6 +122,7 @@
|
|
|
102
122
|
"$defs": {
|
|
103
123
|
"route": {
|
|
104
124
|
"type": "object",
|
|
125
|
+
"additionalProperties": false,
|
|
105
126
|
"properties": {
|
|
106
127
|
"title": { "type": "string", "description": "Display title" },
|
|
107
128
|
"href": { "type": "string", "description": "URL path" },
|
|
@@ -112,6 +133,7 @@
|
|
|
112
133
|
"context": {
|
|
113
134
|
"type": "object",
|
|
114
135
|
"description": "Context metadata for sidebar section",
|
|
136
|
+
"additionalProperties": false,
|
|
115
137
|
"properties": {
|
|
116
138
|
"icon": { "type": "string", "description": "Lucide icon name" },
|
|
117
139
|
"title": { "type": "string", "description": "Context switcher title" },
|
|
@@ -125,6 +147,56 @@
|
|
|
125
147
|
}
|
|
126
148
|
},
|
|
127
149
|
"required": ["title", "href"]
|
|
150
|
+
},
|
|
151
|
+
"hero": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"description": "Hero section configuration for home page",
|
|
154
|
+
"additionalProperties": false,
|
|
155
|
+
"properties": {
|
|
156
|
+
"tagline": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "Tagline displayed above the headline (e.g., product name)"
|
|
159
|
+
},
|
|
160
|
+
"headline": { "type": "string", "description": "Main heading text" },
|
|
161
|
+
"description": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"description": "Description text displayed below the headline"
|
|
164
|
+
},
|
|
165
|
+
"actions": {
|
|
166
|
+
"type": "array",
|
|
167
|
+
"description": "Action buttons displayed in the hero section",
|
|
168
|
+
"items": { "$ref": "#/$defs/heroAction" }
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"required": ["headline"]
|
|
172
|
+
},
|
|
173
|
+
"heroAction": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"description": "Action button in hero section",
|
|
176
|
+
"additionalProperties": false,
|
|
177
|
+
"properties": {
|
|
178
|
+
"text": { "type": "string", "description": "Button label" },
|
|
179
|
+
"link": { "type": "string", "description": "Button link URL" },
|
|
180
|
+
"theme": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"description": "Button color theme",
|
|
183
|
+
"enum": ["primary", "secondary", "ghost"]
|
|
184
|
+
},
|
|
185
|
+
"icon": { "type": "string", "description": "Lucide icon name" }
|
|
186
|
+
},
|
|
187
|
+
"required": ["text", "link"]
|
|
188
|
+
},
|
|
189
|
+
"homeFeature": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"description": "Feature card for home page",
|
|
192
|
+
"additionalProperties": false,
|
|
193
|
+
"properties": {
|
|
194
|
+
"icon": { "type": "string", "description": "Lucide icon name" },
|
|
195
|
+
"title": { "type": "string", "description": "Feature title" },
|
|
196
|
+
"description": { "type": "string", "description": "Feature description" },
|
|
197
|
+
"link": { "type": "string", "description": "Feature link URL" }
|
|
198
|
+
},
|
|
199
|
+
"required": ["title", "description"]
|
|
128
200
|
}
|
|
129
201
|
}
|
|
130
202
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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,8 +51,8 @@
|
|
|
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",
|
|
54
55
|
"@docubook/mdx-content": "^3.2.1",
|
|
55
|
-
"@docubook/ui-react": "^0.1.2",
|
|
56
56
|
"@docubook/core": "^1.7.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
package/template/docs/index.mdx
CHANGED
|
@@ -25,7 +25,7 @@ No heavy abstractions. No complex tooling. Just a minimal layer between your con
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
mkdir my-docs && cd my-docs
|
|
28
|
-
bun add @docubook/flame
|
|
28
|
+
bun add @docubook/flame
|
|
29
29
|
bunx flame init
|
|
30
30
|
bun run dev
|
|
31
31
|
```
|
|
@@ -132,7 +132,7 @@ docs/
|
|
|
132
132
|
|
|
133
133
|
Reference in MDX:
|
|
134
134
|
|
|
135
|
-
```
|
|
135
|
+
```markdown
|
|
136
136
|

|
|
137
137
|
```
|
|
138
138
|
|
|
@@ -183,7 +183,7 @@ Upload the contents of `.docu/dist/` to any static hosting (Netlify, Cloudflare
|
|
|
183
183
|
|
|
184
184
|
Copy `.env.example` to `.env` to customize:
|
|
185
185
|
|
|
186
|
-
```
|
|
186
|
+
```
|
|
187
187
|
# Server port (default: 3000)
|
|
188
188
|
PORT=3000
|
|
189
189
|
|
|
@@ -201,7 +201,7 @@ bun add @sentry/bun
|
|
|
201
201
|
|
|
202
202
|
Then set environment variables:
|
|
203
203
|
|
|
204
|
-
```
|
|
204
|
+
```
|
|
205
205
|
SENTRY_DSN=https://your-dsn@sentry.io/project-id
|
|
206
206
|
```
|
|
207
207
|
|
package/template/docu.json
CHANGED
|
@@ -6,6 +6,46 @@
|
|
|
6
6
|
"baseURL": "http://localhost:3000",
|
|
7
7
|
"favicon": "/docs/assets/images/favicon.ico"
|
|
8
8
|
},
|
|
9
|
+
"home": {
|
|
10
|
+
"hero": {
|
|
11
|
+
"tagline": "#MyDocs",
|
|
12
|
+
"headline": "Documentation",
|
|
13
|
+
"description": "Welcome to your documentation site powered by DocuBook Flame.",
|
|
14
|
+
"actions": [
|
|
15
|
+
{
|
|
16
|
+
"text": "Get Started",
|
|
17
|
+
"link": "/docs/getting-started/introduction",
|
|
18
|
+
"theme": "primary"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"text": "GitHub",
|
|
22
|
+
"link": "https://github.com/gitfromwildan",
|
|
23
|
+
"theme": "ghost",
|
|
24
|
+
"icon": "github"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"features": [
|
|
29
|
+
{
|
|
30
|
+
"icon": "Zap",
|
|
31
|
+
"title": "Lightning Fast",
|
|
32
|
+
"description": "Powered by Bun for instant builds and hot module replacement.",
|
|
33
|
+
"link": "/docs"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"icon": "Paintbrush",
|
|
37
|
+
"title": "Themeable",
|
|
38
|
+
"description": "Customize with Tailwind CSS and DaisyUI for beautiful documentation.",
|
|
39
|
+
"link": "#"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"icon": "Search",
|
|
43
|
+
"title": "Full-Text Search",
|
|
44
|
+
"description": "Built-in search for easy content discovery.",
|
|
45
|
+
"link": "#"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
9
49
|
"navbar": {
|
|
10
50
|
"logo": {
|
|
11
51
|
"src": "/docs/assets/images/docu.svg",
|