@docubook/flame 1.3.8 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import { cn } from "../node/utils";
5
5
  import { Dropdown, DropdownItem } from "@docubook/ui-react/dropdown";
6
- import { routes } from "../node/client-routes";
6
+ import { routes, config as docuConfig } from "../node/client-routes";
7
7
  import { ChevronsUpDown, Check } from "lucide-react";
8
8
  import { renderLucideIcon } from "./Lucide";
9
9
 
@@ -30,6 +30,9 @@ export function Context({ className }: ContextProps) {
30
30
  typeof window !== "undefined" ? window.location.pathname : "/"
31
31
  );
32
32
  const mounted = typeof window !== "undefined";
33
+
34
+ const mode = docuConfig.sidebar?.context || "dropdown";
35
+ if (mode === "separator") return null;
33
36
  const activeRoute = pathname.startsWith("/docs") ? getActiveContextRoute(pathname) : undefined;
34
37
  const contextRoutes = getContextRoutes();
35
38
  const fallbackRoute = routes[0];
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { useState } from "react";
4
4
  import Sublink from "./Sublink";
5
+ import SidebarGroupHeader from "./SidebarGroupHeader";
5
6
  import type { DocuRoute } from "../node/types";
6
7
  import { cn } from "../node/utils";
8
+ import { config as docuConfig } from "../node/client-routes";
7
9
 
8
10
  interface MenuProps {
9
11
  onNavigate?: () => void;
@@ -33,6 +35,85 @@ export default function Menu({ onNavigate, className = "", pathname, routes = []
33
35
 
34
36
  if (!currentPath.startsWith("/docs")) return null;
35
37
 
38
+ const mode = docuConfig.sidebar?.context || "dropdown";
39
+
40
+ // Helper: check if a sublink under a given route matches the current path
41
+ const isItemActive = (itemHref: string, parentRouteHref: string) => {
42
+ const fullHref = `/docs${parentRouteHref}${itemHref}`;
43
+ return currentPath === fullHref || currentPath === `${fullHref}.html`;
44
+ };
45
+
46
+ // Separator mode: render all context sections as group headers + nav items
47
+ if (mode === "separator") {
48
+ const contextRoutes = menuRoutes.filter((r) => r.context);
49
+
50
+ // No context routes defined — fall back to flat list of all routes
51
+ if (contextRoutes.length === 0) {
52
+ return (
53
+ <nav
54
+ aria-label="Documentation navigation"
55
+ className={cn("transition-all duration-200", className)}
56
+ >
57
+ <ul className="flex flex-col gap-1.5 py-4">
58
+ {menuRoutes.map((route) => (
59
+ <li key={route.href}>
60
+ <Sublink
61
+ {...route}
62
+ href={route.href}
63
+ level={0}
64
+ onNavigate={onNavigate}
65
+ parentHref="/docs"
66
+ />
67
+ </li>
68
+ ))}
69
+ </ul>
70
+ </nav>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <nav
76
+ aria-label="Documentation navigation"
77
+ className={cn("transition-all duration-200", className)}
78
+ >
79
+ {contextRoutes.map((route, i) => (
80
+ <div key={route.href} className={i > 0 ? "mt-6 lg:mt-8" : ""}>
81
+ <SidebarGroupHeader
82
+ icon={route.context?.icon}
83
+ title={route.context?.title || route.title}
84
+ />
85
+ <ul className="border-base-300 flex flex-col gap-1.5 border-l-2 pb-0.5 pl-3 pt-0.5">
86
+ {route.items?.map((item) => {
87
+ const isActive = isItemActive(item.href, route.href);
88
+ return (
89
+ <li key={item.href}>
90
+ <div
91
+ className={cn(
92
+ "-ml-[14px] border-l-2",
93
+ isActive ? "border-primary" : "border-transparent"
94
+ )}
95
+ >
96
+ <div className="pl-3">
97
+ <Sublink
98
+ {...item}
99
+ href={item.href}
100
+ level={0}
101
+ onNavigate={onNavigate}
102
+ parentHref={`/docs${route.href}`}
103
+ />
104
+ </div>
105
+ </div>
106
+ </li>
107
+ );
108
+ })}
109
+ </ul>
110
+ </div>
111
+ ))}
112
+ </nav>
113
+ );
114
+ }
115
+
116
+ // Dropdown mode: render only the active context section
36
117
  const isDocsRoot = currentPath === "/docs" || currentPath === "/docs/";
37
118
  const currentContext = isDocsRoot
38
119
  ? menuRoutes[0]?.href.replace(/^\/+|\/+$/, "")
@@ -0,0 +1,21 @@
1
+ import { renderLucideIcon } from "./Lucide";
2
+
3
+ interface SidebarGroupHeaderProps {
4
+ icon?: string;
5
+ title: string;
6
+ }
7
+
8
+ export default function SidebarGroupHeader({ icon, title }: SidebarGroupHeaderProps) {
9
+ return (
10
+ <div className="sidebar-group-header mb-1.5 flex items-center gap-2.5 font-medium text-gray-900 dark:text-gray-200">
11
+ {icon && (
12
+ <span className="flex h-4 w-4 shrink-0 items-center justify-center">
13
+ {renderLucideIcon(icon, "h-3.5 w-3.5")}
14
+ </span>
15
+ )}
16
+ <h3 className="sidebar-title font-[inherit] text-[length:inherit] leading-[inherit]">
17
+ <span>{title}</span>
18
+ </h3>
19
+ </div>
20
+ );
21
+ }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import { useState, useRef, useEffect } from "react";
4
4
  import { ChevronDown } from "lucide-react";
5
5
  import Anchor from "./Anchor";
6
6
  import type { DocuRoute } from "../node/types";
@@ -35,11 +35,17 @@ export default function Sublink({
35
35
 
36
36
  // Shared padding based on nesting level
37
37
  const levelPadding = cn(level === 1 && "pl-2", level === 2 && "pl-4", level >= 3 && "pl-6");
38
+ const isActive = currentPathname === fullHref || currentPathname === `${fullHref}.html`;
39
+ const activeRef = useRef<HTMLDivElement>(null);
40
+
41
+ useEffect(() => {
42
+ if (isActive && activeRef.current) {
43
+ activeRef.current.scrollIntoView({ block: "nearest" });
44
+ }
45
+ }, [isActive]);
38
46
 
39
47
  // Leaf node (no children)
40
48
  if (!items) {
41
- const isActive = currentPathname === fullHref || currentPathname === `${fullHref}.html`;
42
-
43
49
  const link = (
44
50
  <Anchor
45
51
  href={fullHref}
@@ -51,8 +57,10 @@ export default function Sublink({
51
57
  {title}
52
58
  </Anchor>
53
59
  );
60
+
54
61
  return (
55
62
  <div
63
+ ref={activeRef}
56
64
  className={cn(
57
65
  "py-1.5",
58
66
  levelPadding,
@@ -67,7 +75,7 @@ export default function Sublink({
67
75
 
68
76
  // Section with children
69
77
  return (
70
- <div className={cn("flex flex-col", levelPadding)}>
78
+ <div ref={isActive ? activeRef : undefined} className={cn("flex flex-col", levelPadding)}>
71
79
  {/* Section header */}
72
80
  <button
73
81
  type="button"
@@ -1,5 +1,5 @@
1
- import type { DocuRoute } from "./types";
1
+ import type { DocuRoute, DocuConfig } from "./types";
2
2
  import docuConfig from "../../docu.json" with { type: "json" };
3
3
 
4
4
  export const routes: DocuRoute[] = docuConfig.routes || [];
5
- export const config = docuConfig;
5
+ export const config = docuConfig as unknown as DocuConfig;
@@ -4,11 +4,32 @@ import type { SocialLink } from "./types";
4
4
  const docuConfig = loadDocuConfig();
5
5
 
6
6
  export function getEditLink(url: string, filePath: string): string {
7
- const configPath = docuConfig?.repo?.path || "blob/main/{filePath}";
7
+ const configPath = docuConfig?.repo?.path || detectPlatformPath(url);
8
8
  const encodedPath = filePath.replace(/^\//, "").split("/").map(encodeURIComponent).join("/");
9
9
  return `${url}/${configPath}`.replace("{filePath}", encodedPath);
10
10
  }
11
11
 
12
+ /**
13
+ * Detect the default edit path template from the repo URL hostname.
14
+ * Supports GitHub, GitLab, Bitbucket, Gitea (cloud + self-hosted),
15
+ * Gogs, Forgejo, and Codeberg.
16
+ * Falls back to Gitea-style for unknown hosts — override via repo.path.
17
+ */
18
+ export function detectPlatformPath(url: string): string {
19
+ try {
20
+ const host = new URL(url).hostname;
21
+ if (host === "github.com") return "blob/main/{filePath}";
22
+ if (host === "gitlab.com") return "-/blob/main/{filePath}";
23
+ if (host === "bitbucket.org") return "src/main/{filePath}";
24
+ if (host === "gitea.com") return "src/branch/main/{filePath}";
25
+ if (host === "codeberg.org") return "src/branch/main/{filePath}";
26
+ // Gogs, Forgejo, or any self-hosted Gitea-compatible forge
27
+ return "src/branch/main/{filePath}";
28
+ } catch {
29
+ return "blob/main/{filePath}";
30
+ }
31
+ }
32
+
12
33
  export function isEditEnabled(): boolean {
13
34
  return docuConfig?.repo?.edit ?? false;
14
35
  }
@@ -34,6 +34,13 @@ export interface DocuFooter {
34
34
  social: SocialLink[];
35
35
  }
36
36
 
37
+ export interface DocuSidebar {
38
+ /** How context sections are displayed.
39
+ * "dropdown" — context switcher dropdown (default)
40
+ * "separator" — inline group headers in sidebar */
41
+ context?: "dropdown" | "separator";
42
+ }
43
+
37
44
  export interface RepoConfig {
38
45
  url: string;
39
46
  path: string;
@@ -75,6 +82,7 @@ export interface DocuConfig {
75
82
  navbar: DocuNavbar;
76
83
  footer: DocuFooter;
77
84
  repo: RepoConfig;
85
+ sidebar?: DocuSidebar;
78
86
  routes: DocuRoute[];
79
87
  themes?: {
80
88
  colors: ThemeConfig;
package/README.md CHANGED
@@ -105,14 +105,91 @@ bun run deploy # Build + prepare for GitHub Pages
105
105
  },
106
106
  "repo": {
107
107
  "url": "https://github.com/you/repo",
108
- "path": "blob/main/{filePath}",
109
108
  "edit": true
110
109
  },
111
110
  "routes": []
112
111
  }
113
112
  ```
114
113
 
115
- ### Home Page
114
+ ### Repo & Edit Links
115
+
116
+ The `repo` section enables **"Edit this page"** links on every doc page, pointing directly to the source file in your repository.
117
+
118
+ ```json
119
+ {
120
+ "repo": {
121
+ "url": "https://github.com/you/repo",
122
+ "edit": true
123
+ }
124
+ }
125
+ ```
126
+
127
+ | Property | Type | Required | Description |
128
+ |----------|------|----------|-------------|
129
+ | `url` | `string` (URI) | ✅ Yes | Base URL of your repository |
130
+ | `edit` | `boolean` | ✅ Yes | Show or hide the edit link on pages |
131
+ | `path` | `string` | No | Path template override — see below |
132
+
133
+ #### Platform Auto-Detection
134
+
135
+ When `path` is omitted, Flame detects the correct path format from `url` automatically:
136
+
137
+ | Platform | Domain | Auto-generated path |
138
+ |----------|--------|---------------------|
139
+ | GitHub | `github.com` | `blob/main/{filePath}` |
140
+ | GitLab | `gitlab.com` | `-/blob/main/{filePath}` |
141
+ | Bitbucket | `bitbucket.org` | `src/main/{filePath}` |
142
+ | Gitea Cloud | `gitea.com` | `src/branch/main/{filePath}` |
143
+ | Codeberg (Forgejo) | `codeberg.org` | `src/branch/main/{filePath}` |
144
+ | Gogs / self-hosted Gitea / Forgejo | any other host | `src/branch/main/{filePath}` |
145
+
146
+ For most single-repo projects this is all you need — just set `url` and `edit: true`.
147
+
148
+ #### When to set `path` manually
149
+
150
+ Override `path` when auto-detection is not enough. The value must contain `{filePath}` as a placeholder:
151
+
152
+ **Monorepo** — docs live in a subdirectory, not the repo root:
153
+
154
+ ```json
155
+ {
156
+ "repo": {
157
+ "url": "https://github.com/org/monorepo",
158
+ "path": "blob/main/apps/docs/{filePath}",
159
+ "edit": true
160
+ }
161
+ }
162
+ ```
163
+
164
+ **Non-default branch** — your default branch is not `main`:
165
+
166
+ ```json
167
+ {
168
+ "repo": {
169
+ "url": "https://github.com/you/repo",
170
+ "path": "blob/master/{filePath}",
171
+ "edit": true
172
+ }
173
+ }
174
+ ```
175
+
176
+ **Self-hosted GitLab on a custom domain** — auto-detect falls back to Gitea format, which is wrong for GitLab:
177
+
178
+ ```json
179
+ {
180
+ "repo": {
181
+ "url": "https://git.mycompany.com/team/repo",
182
+ "path": "-/blob/main/{filePath}",
183
+ "edit": true
184
+ }
185
+ }
186
+ ```
187
+
188
+ > **Rule of thumb:** if your docs are at the root of the repo and the branch is `main`, skip `path` — auto-detect handles it. Add `path` only when you need to point to a subdirectory, a different branch, or a self-hosted platform with a non-standard URL format.
189
+
190
+
191
+
192
+ ### Homepage
116
193
 
117
194
  The `home` section configures your landing page with a hero section and feature cards:
118
195
 
@@ -212,6 +289,43 @@ Theme resolution follows this order (first match wins):
212
289
 
213
290
  ---
214
291
 
292
+ ### Sidebar
293
+
294
+ Controls how documentation sections appear in the sidebar. Defaults to **dropdown** mode when not configured.
295
+
296
+ To switch to **separator** mode, add `sidebar` to the top level of your `docu.json`:
297
+
298
+ ```json
299
+ {
300
+ "sidebar": {
301
+ "context": "separator"
302
+ }
303
+ }
304
+ ```
305
+
306
+ | Mode | Description |
307
+ |------|-------------|
308
+ | `"dropdown"` (default) | Compact view — a dropdown at the top of the sidebar lets users switch between sections. Only the active section's items are shown. |
309
+ | `"separator"` | All sections visible — group header (icon + title) with tree connector line. Items nest under their section. |
310
+
311
+ ```
312
+ Default (dropdown) Separator
313
+ ┌──────────────────┐ 📖 Guides
314
+ │ 📖 Guides ▼ │ │
315
+ ├──────────────────┤ ├─ Introduction
316
+ │ Introduction │ ├─ Installation
317
+ │ Installation │ │
318
+ └──────────────────┘ 🧩 Markdown
319
+ │
320
+ ├─ Accordion
321
+ ├─ Button
322
+ └─ Card
323
+ ```
324
+
325
+ > Omit `sidebar` entirely → dropdown mode. Set `"context": "separator"` → separator mode. Mode is static per page, no runtime switching.
326
+
327
+ ---
328
+
215
329
  ### Routes
216
330
 
217
331
  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.
@@ -241,6 +355,8 @@ To define navigation manually, populate the `routes` array:
241
355
 
242
356
  > Manual routes take priority — if `routes` has entries, folder scanning is skipped entirely.
243
357
 
358
+ > Context on a route (`icon`, `title`, `description`) provides metadata for the sidebar context switcher. In **dropdown** mode it fills the dropdown; in **separator** mode it renders the group header + tree. The icon name must match a [Lucide icon](https://lucide.dev/icons) export (e.g., `"BookOpen"`, `"Layers"`).
359
+
244
360
  ---
245
361
 
246
362
  ## Routing
package/docu.schema.json CHANGED
@@ -105,14 +105,49 @@
105
105
  "description": "Repository configuration for edit links",
106
106
  "additionalProperties": false,
107
107
  "properties": {
108
- "url": { "type": "string", "description": "Repository URL" },
108
+ "url": {
109
+ "type": "string",
110
+ "format": "uri",
111
+ "description": "Repository base URL. Supported platforms: GitHub, GitLab, Bitbucket, Gitea (gitea.com), Codeberg (codeberg.org), Gogs, Forgejo, or any self-hosted Gitea-compatible forge.",
112
+ "examples": [
113
+ "https://github.com/owner/repo",
114
+ "https://gitlab.com/owner/repo",
115
+ "https://bitbucket.org/owner/repo",
116
+ "https://gitea.com/owner/repo",
117
+ "https://codeberg.org/owner/repo"
118
+ ]
119
+ },
109
120
  "path": {
110
121
  "type": "string",
111
- "description": "Path template for edit links. Use {filePath} as placeholder."
122
+ "description": "Path template for edit links. Must contain {filePath} as a placeholder. Format: {platform-path}/{optional-subdir}/{filePath}. Leave empty to auto-detect from repo.url hostname.",
123
+ "pattern": "\\{filePath\\}",
124
+ "examples": [
125
+ "blob/main/{filePath}",
126
+ "blob/main/apps/web/{filePath}",
127
+ "blob/develop/{filePath}",
128
+ "-/blob/main/{filePath}",
129
+ "-/blob/main/docs/{filePath}",
130
+ "src/main/{filePath}",
131
+ "src/branch/main/{filePath}",
132
+ "src/branch/main/docs/{filePath}"
133
+ ]
112
134
  },
113
135
  "edit": { "type": "boolean", "description": "Enable edit link on pages" }
114
136
  }
115
137
  },
138
+ "sidebar": {
139
+ "type": "object",
140
+ "description": "Sidebar configuration",
141
+ "additionalProperties": false,
142
+ "properties": {
143
+ "context": {
144
+ "type": "string",
145
+ "enum": ["dropdown", "separator"],
146
+ "default": "dropdown",
147
+ "description": "How context sections are displayed in sidebar — dropdown (switcher) or separator (inline group headers)"
148
+ }
149
+ }
150
+ },
116
151
  "routes": {
117
152
  "type": "array",
118
153
  "description": "Documentation navigation routes. Leave empty for auto-detection from docs/ folder.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "1.3.8",
3
+ "version": "1.4.1",
4
4
  "description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,8 +52,8 @@
52
52
  "react": "^19.2.7",
53
53
  "react-dom": "^19.2.7",
54
54
  "unified": "^11.0.0",
55
- "@docubook/core": "^1.7.2",
56
- "@docubook/mdx-content": "^3.2.2",
55
+ "@docubook/core": "^1.8.0",
56
+ "@docubook/mdx-content": "^3.3.0",
57
57
  "@docubook/themes-colors": "^0.10.2",
58
58
  "@docubook/ui-react": "^0.1.4"
59
59
  },