@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.
@@ -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>
@@ -141,7 +141,11 @@ export function MobileBar({
141
141
  }
142
142
  >
143
143
  {(docuConfig.navbar?.menu || []).map((item) => (
144
- <li key={item.href} className="text-base-content/80 hover:text-base-content">
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 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}
@@ -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
- level >= 2 && "border-l-2 pl-3",
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
+ }
@@ -0,0 +1,2 @@
1
+ export { Hero } from "./Hero";
2
+ export { Features } from "./Features";
@@ -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}`;
@@ -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;
@@ -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
@@ -16,7 +16,7 @@
16
16
 
17
17
  ```bash
18
18
  mkdir my-docs && cd my-docs
19
- bun add @docubook/flame@rc
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
- | 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 |
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.0.0",
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": {
@@ -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@rc
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
- ```mdx
135
+ ```markdown
136
136
  ![Screenshot](/docs/assets/images/screenshot.png)
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
- ```env
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
- ```env
204
+ ```
205
205
  SENTRY_DSN=https://your-dsn@sentry.io/project-id
206
206
  ```
207
207
 
@@ -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",