@docubook/flame 1.0.0-beta.70 → 1.0.0-beta.80

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.
@@ -0,0 +1,21 @@
1
+ export { PaginationDocs } from "./pagination-docs";
2
+ export {
3
+ Pagination,
4
+ PaginationItem,
5
+ PaginationButtons,
6
+ PaginationRange,
7
+ PaginationInfo,
8
+ PaginationFull,
9
+ } from "./pagination-numbers";
10
+ export { getPaginationRange } from "./types";
11
+ export type {
12
+ PaginationRootProps as PaginationProps,
13
+ PaginationItemProps,
14
+ PaginationRangeProps,
15
+ PaginationButtonsProps,
16
+ PaginationInfoProps,
17
+ PaginationFullProps,
18
+ PaginationDocsProps,
19
+ PaginationSize,
20
+ PaginationVariant,
21
+ } from "./types";
@@ -0,0 +1,39 @@
1
+ import { cn } from "../../../node/utils";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import type { PaginationDocsProps } from "./types";
4
+
5
+ export function PaginationDocs({ prev, next, className }: PaginationDocsProps) {
6
+ return (
7
+ <div className={cn("grid grow grid-cols-1 gap-4 py-8 sm:grid-cols-2", className)}>
8
+ <div>
9
+ {prev && (
10
+ <a
11
+ href={prev.href}
12
+ className="btn btn-outline border-base-300 items-start! py-2! h-auto w-full flex-col pl-4 no-underline"
13
+ >
14
+ <span className="text-muted-foreground flex items-center text-xs">
15
+ <ChevronLeft className="mr-1 h-4 w-4" />
16
+ Previous
17
+ </span>
18
+ <span className="text-base-content mt-1 text-sm font-medium">{prev.title}</span>
19
+ </a>
20
+ )}
21
+ </div>
22
+
23
+ <div>
24
+ {next && (
25
+ <a
26
+ href={next.href}
27
+ className="btn btn-outline border-base-300 items-end! py-2! h-auto w-full flex-col pr-4 no-underline"
28
+ >
29
+ <span className="text-muted-foreground flex items-center text-xs">
30
+ Next
31
+ <ChevronRight className="ml-1 h-4 w-4" />
32
+ </span>
33
+ <span className="text-base-content mt-1 text-sm font-medium">{next.title}</span>
34
+ </a>
35
+ )}
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -1,136 +1,17 @@
1
1
  "use client";
2
2
 
3
- import { cn } from "../../node/utils";
3
+ import { cn } from "../../../node/utils";
4
4
  import { ChevronLeft, ChevronRight } from "lucide-react";
5
- import { useMemo, type ReactNode } from "react";
6
-
7
- type PaginationSize = "lg" | "md" | "sm" | "xs";
8
- type PaginationVariant = "default" | "square" | "rounded";
9
-
10
- interface PaginationRootProps {
11
- children?: ReactNode;
12
- className?: string;
13
- size?: PaginationSize;
14
- variant?: PaginationVariant;
15
- }
16
-
17
- interface PaginationItemProps {
18
- children: ReactNode;
19
- active?: boolean;
20
- disabled?: boolean;
21
- onClick?: () => void;
22
- className?: string;
23
- size?: PaginationSize;
24
- variant?: PaginationVariant;
25
- "aria-label"?: string;
26
- }
27
-
28
- interface PaginationRangeProps {
29
- start: number;
30
- end: number;
31
- total: number;
32
- current: number;
33
- onPageChange: (page: number) => void;
34
- siblingCount?: number;
35
- className?: string;
36
- size?: PaginationSize;
37
- variant?: PaginationVariant;
38
- }
39
-
40
- interface PaginationButtonsProps {
41
- page: number;
42
- totalPages: number;
43
- onPageChange: (page: number) => void;
44
- className?: string;
45
- size?: PaginationSize;
46
- showFirstLast?: boolean;
47
- showPrevNext?: boolean;
48
- prevLabel?: string;
49
- nextLabel?: string;
50
- firstLabel?: string;
51
- lastLabel?: string;
52
- disabled?: boolean;
53
- }
54
-
55
- interface PaginationInfoProps {
56
- current: number;
57
- total: number;
58
- pageSize?: number;
59
- className?: string;
60
- label?: string;
61
- showTotal?: boolean;
62
- }
63
-
64
- interface PaginationFullProps {
65
- current: number;
66
- total: number;
67
- pageSize?: number;
68
- onPageChange: (page: number) => void;
69
- siblingCount?: number;
70
- className?: string;
71
- size?: PaginationSize;
72
- variant?: PaginationVariant;
73
- showFirstLast?: boolean;
74
- showPrevNext?: boolean;
75
- infoClassName?: string;
76
- }
77
-
78
- interface PaginationDocsProps {
79
- prev?: { href: string; title: string };
80
- next?: { href: string; title: string };
81
- className?: string;
82
- }
83
-
84
- function getPaginationRange({
85
- totalCount,
86
- pageSize,
87
- siblingCount = 1,
88
- currentPage,
89
- }: {
90
- totalCount: number;
91
- pageSize: number;
92
- siblingCount?: number;
93
- currentPage: number;
94
- }): (number | "ellipsis")[] {
95
- const totalPages = Math.ceil(totalCount / pageSize);
96
- const DOTS = "ellipsis" as const;
97
-
98
- if (totalPages === 1) {
99
- return [1];
100
- }
101
-
102
- const totalPageNumbers = siblingCount + 5;
103
-
104
- if (totalPages < totalPageNumbers) {
105
- return Array.from({ length: totalPages }, (_, i) => i + 1);
106
- }
107
-
108
- const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
109
- const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages);
110
-
111
- const showLeftDots = leftSiblingIndex > 2;
112
- const showRightDots = rightSiblingIndex < totalPages - 1;
113
-
114
- if (!showLeftDots && showRightDots) {
115
- const leftRange = Array.from({ length: 3 }, (_, i) => i + 1);
116
- return [...leftRange, DOTS, totalPages - 1, totalPages];
117
- }
118
-
119
- if (showLeftDots && !showRightDots) {
120
- const rightRange = Array.from({ length: 3 }, (_, i) => totalPages - 2 + i);
121
- return [1, 2, DOTS, ...rightRange];
122
- }
123
-
124
- if (showLeftDots && showRightDots) {
125
- const middleRange = Array.from(
126
- { length: rightSiblingIndex - leftSiblingIndex + 1 },
127
- (_, i) => leftSiblingIndex + i
128
- );
129
- return [1, 2, DOTS, ...middleRange, DOTS, totalPages - 1, totalPages];
130
- }
131
-
132
- return [1];
133
- }
5
+ import { useMemo } from "react";
6
+ import {
7
+ getPaginationRange,
8
+ type PaginationRootProps,
9
+ type PaginationItemProps,
10
+ type PaginationButtonsProps,
11
+ type PaginationRangeProps,
12
+ type PaginationInfoProps,
13
+ type PaginationFullProps,
14
+ } from "./types";
134
15
 
135
16
  export function Pagination({
136
17
  children,
@@ -496,53 +377,3 @@ export function PaginationFull({
496
377
  </div>
497
378
  );
498
379
  }
499
-
500
- export function PaginationDocs({ prev, next, className }: PaginationDocsProps) {
501
- return (
502
- <div className={cn("grid grow grid-cols-1 gap-4 py-8 sm:grid-cols-2", className)}>
503
- <div>
504
- {prev && (
505
- <a
506
- href={prev.href}
507
- className="btn btn-outline border-base-300 items-start! py-2! h-auto w-full flex-col pl-4 no-underline"
508
- >
509
- <span className="text-muted-foreground flex items-center text-xs">
510
- <ChevronLeft className="mr-1 h-4 w-4" />
511
- Previous
512
- </span>
513
- <span className="text-base-content mt-1 text-sm font-medium">{prev.title}</span>
514
- </a>
515
- )}
516
- </div>
517
-
518
- <div>
519
- {next && (
520
- <a
521
- href={next.href}
522
- className="btn btn-outline border-base-300 items-end! py-2! h-auto w-full flex-col pr-4 no-underline"
523
- >
524
- <span className="text-muted-foreground flex items-center text-xs">
525
- Next
526
- <ChevronRight className="ml-1 h-4 w-4" />
527
- </span>
528
- <span className="text-base-content mt-1 text-sm font-medium">{next.title}</span>
529
- </a>
530
- )}
531
- </div>
532
- </div>
533
- );
534
- }
535
-
536
- export type {
537
- PaginationRootProps as PaginationProps,
538
- PaginationItemProps,
539
- PaginationRangeProps,
540
- PaginationButtonsProps,
541
- PaginationInfoProps,
542
- PaginationFullProps,
543
- PaginationDocsProps,
544
- PaginationSize,
545
- PaginationVariant,
546
- };
547
-
548
- export { getPaginationRange };
@@ -0,0 +1,129 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type PaginationSize = "lg" | "md" | "sm" | "xs";
4
+ export type PaginationVariant = "default" | "square" | "rounded";
5
+
6
+ export interface PaginationRootProps {
7
+ children?: ReactNode;
8
+ className?: string;
9
+ size?: PaginationSize;
10
+ variant?: PaginationVariant;
11
+ }
12
+
13
+ export interface PaginationItemProps {
14
+ children: ReactNode;
15
+ active?: boolean;
16
+ disabled?: boolean;
17
+ onClick?: () => void;
18
+ className?: string;
19
+ size?: PaginationSize;
20
+ variant?: PaginationVariant;
21
+ "aria-label"?: string;
22
+ }
23
+
24
+ export interface PaginationRangeProps {
25
+ start: number;
26
+ end: number;
27
+ total: number;
28
+ current: number;
29
+ onPageChange: (page: number) => void;
30
+ siblingCount?: number;
31
+ className?: string;
32
+ size?: PaginationSize;
33
+ variant?: PaginationVariant;
34
+ }
35
+
36
+ export interface PaginationButtonsProps {
37
+ page: number;
38
+ totalPages: number;
39
+ onPageChange: (page: number) => void;
40
+ className?: string;
41
+ size?: PaginationSize;
42
+ showFirstLast?: boolean;
43
+ showPrevNext?: boolean;
44
+ prevLabel?: string;
45
+ nextLabel?: string;
46
+ firstLabel?: string;
47
+ lastLabel?: string;
48
+ disabled?: boolean;
49
+ }
50
+
51
+ export interface PaginationInfoProps {
52
+ current: number;
53
+ total: number;
54
+ pageSize?: number;
55
+ className?: string;
56
+ label?: string;
57
+ showTotal?: boolean;
58
+ }
59
+
60
+ export interface PaginationFullProps {
61
+ current: number;
62
+ total: number;
63
+ pageSize?: number;
64
+ onPageChange: (page: number) => void;
65
+ siblingCount?: number;
66
+ className?: string;
67
+ size?: PaginationSize;
68
+ variant?: PaginationVariant;
69
+ showFirstLast?: boolean;
70
+ showPrevNext?: boolean;
71
+ infoClassName?: string;
72
+ }
73
+
74
+ export interface PaginationDocsProps {
75
+ prev?: { href: string; title: string };
76
+ next?: { href: string; title: string };
77
+ className?: string;
78
+ }
79
+
80
+ export function getPaginationRange({
81
+ totalCount,
82
+ pageSize,
83
+ siblingCount = 1,
84
+ currentPage,
85
+ }: {
86
+ totalCount: number;
87
+ pageSize: number;
88
+ siblingCount?: number;
89
+ currentPage: number;
90
+ }): (number | "ellipsis")[] {
91
+ const totalPages = Math.ceil(totalCount / pageSize);
92
+ const DOTS = "ellipsis" as const;
93
+
94
+ if (totalPages === 1) {
95
+ return [1];
96
+ }
97
+
98
+ const totalPageNumbers = siblingCount + 5;
99
+
100
+ if (totalPages < totalPageNumbers) {
101
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
102
+ }
103
+
104
+ const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
105
+ const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages);
106
+
107
+ const showLeftDots = leftSiblingIndex > 2;
108
+ const showRightDots = rightSiblingIndex < totalPages - 1;
109
+
110
+ if (!showLeftDots && showRightDots) {
111
+ const leftRange = Array.from({ length: 3 }, (_, i) => i + 1);
112
+ return [...leftRange, DOTS, totalPages - 1, totalPages];
113
+ }
114
+
115
+ if (showLeftDots && !showRightDots) {
116
+ const rightRange = Array.from({ length: 3 }, (_, i) => totalPages - 2 + i);
117
+ return [1, 2, DOTS, ...rightRange];
118
+ }
119
+
120
+ if (showLeftDots && showRightDots) {
121
+ const middleRange = Array.from(
122
+ { length: rightSiblingIndex - leftSiblingIndex + 1 },
123
+ (_, i) => leftSiblingIndex + i
124
+ );
125
+ return [1, 2, DOTS, ...middleRange, DOTS, totalPages - 1, totalPages];
126
+ }
127
+
128
+ return [1];
129
+ }
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  import { join, dirname } from "node:path";
5
5
  import React from "react";
6
6
  import { renderToString } from "react-dom/server";
7
- import { compileMdx } from "./mdx";
7
+ import { compileMdx, getGitLastModifiedBatch } from "./mdx";
8
8
  import {
9
9
  DOCS_DIR,
10
10
  DIST_DIR,
@@ -103,10 +103,15 @@ function htmlShell(title: string, description: string, body: string): string {
103
103
  });
104
104
  }
105
105
 
106
- async function renderDocsPage(slug: string, rawMdx: string, filePath: string): Promise<string> {
106
+ async function renderDocsPage(
107
+ slug: string,
108
+ rawMdx: string,
109
+ filePath: string,
110
+ gitDates?: Map<string, string>
111
+ ): Promise<string> {
107
112
  let result;
108
113
  try {
109
- result = await compileMdx(rawMdx, filePath);
114
+ result = await compileMdx(rawMdx, filePath, gitDates);
110
115
  } catch (err) {
111
116
  const msg = err instanceof Error ? err.message : "Unknown MDX error";
112
117
  throw new Error(`MDX Error in: docs/${slug}.mdx\n${msg}`, { cause: err });
@@ -194,6 +199,19 @@ async function build() {
194
199
  logger.spinner.start("Building pages...");
195
200
  t = performance.now();
196
201
 
202
+ const allRelPaths = mdxFiles
203
+ .map((f) => {
204
+ const mdxPath1 = join(DOCS_DIR, f.path, "index.mdx");
205
+ const mdxPath2 = join(DOCS_DIR, `${f.path}.mdx`);
206
+ const mdxPath3 = join(DOCS_DIR, `${f.path}.md`);
207
+ for (const p of [mdxPath1, mdxPath2, mdxPath3]) {
208
+ if (existsSync(p)) return p.replace(PROJECT_ROOT + "/", "");
209
+ }
210
+ return null;
211
+ })
212
+ .filter((p): p is string => p !== null);
213
+ const gitDates = await getGitLastModifiedBatch(allRelPaths);
214
+
197
215
  const CONCURRENCY = Math.max(1, parseInt(process.env.BUILD_CONCURRENCY || "10", 10) || 10);
198
216
  const buildTasks = [];
199
217
  const errors: string[] = [];
@@ -232,7 +250,7 @@ async function build() {
232
250
 
233
251
  buildTasks.push(async () => {
234
252
  try {
235
- const html = await renderDocsPage(capturedFile.path, capturedRawMdx, relPath);
253
+ const html = await renderDocsPage(capturedFile.path, capturedRawMdx, relPath, gitDates);
236
254
  const outputPath = join(DIST_DIR, "docs", `${capturedFile.path}.html`);
237
255
  await mkdir(dirname(outputPath), { recursive: true });
238
256
  await writeFile(outputPath, html);
@@ -258,7 +276,7 @@ async function build() {
258
276
  const indexMdxPath = join(DOCS_DIR, "index.mdx");
259
277
  const indexRaw = await readFile(indexMdxPath, "utf-8");
260
278
  const indexRelPath = indexMdxPath.replace(PROJECT_ROOT + "/", "");
261
- const indexHtml = await renderDocsPage("", indexRaw, indexRelPath);
279
+ const indexHtml = await renderDocsPage("", indexRaw, indexRelPath, gitDates);
262
280
  await mkdir(join(DIST_DIR, "docs"), { recursive: true });
263
281
  await writeFile(join(DIST_DIR, "docs", "index.html"), indexHtml);
264
282
  } catch (err) {
package/.docu/node/mdx.ts CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  MDXRemote,
9
9
  } from "@docubook/core";
10
10
  import { createMdxComponents } from "@docubook/mdx-content";
11
- import { getGitLastModified } from "./utils";
11
+ import { getGitLastModified, getGitLastModifiedBatch } from "./utils";
12
+
13
+ export { getGitLastModifiedBatch };
12
14
 
13
15
  export interface MdxResult {
14
16
  content: React.ReactElement;
@@ -17,7 +19,11 @@ export interface MdxResult {
17
19
  tocs: ReturnType<typeof extractTocsFromRawMdx>;
18
20
  }
19
21
 
20
- export async function compileMdx(rawMdx: string, filePath: string): Promise<MdxResult> {
22
+ export async function compileMdx(
23
+ rawMdx: string,
24
+ filePath: string,
25
+ gitDates?: Map<string, string>
26
+ ): Promise<MdxResult> {
21
27
  const tocs = extractTocsFromRawMdx(rawMdx);
22
28
  const { frontmatter, strippedContent } = extractFrontmatterWithContent<{
23
29
  title?: string;
@@ -40,7 +46,11 @@ export async function compileMdx(rawMdx: string, filePath: string): Promise<MdxR
40
46
  components,
41
47
  });
42
48
 
43
- const date = frontmatter.date || (await getGitLastModified(filePath)) || undefined;
49
+ const date =
50
+ frontmatter.date ||
51
+ gitDates?.get(filePath) ||
52
+ (await getGitLastModified(filePath)) ||
53
+ undefined;
44
54
 
45
55
  return {
46
56
  content,
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
15
15
  import { resolve, join } from "node:path";
16
+ import { extractFrontmatterWithContent } from "@docubook/core";
16
17
  import { DOCS_DIR, ASSETS_DIR, loadDocuConfig } from "./paths";
17
18
 
18
19
  const docuConfig = loadDocuConfig();
@@ -37,25 +38,6 @@ interface Frontmatter {
37
38
  description?: string;
38
39
  }
39
40
 
40
- function parseFrontmatter(raw: string): { frontmatter: Frontmatter; content: string } {
41
- if (!raw.startsWith("---")) return { frontmatter: {}, content: raw };
42
- const end = raw.indexOf("---", 3);
43
- if (end < 0) return { frontmatter: {}, content: raw };
44
-
45
- const fm: Frontmatter = {};
46
- const lines = raw.slice(3, end).trim().split("\n");
47
- for (const line of lines) {
48
- const match = line.match(/^(\w+):\s*(.+)$/);
49
- if (match) {
50
- const [, key, value] = match;
51
- if (key === "title" || key === "description") {
52
- fm[key] = value.trim();
53
- }
54
- }
55
- }
56
- return { frontmatter: fm, content: raw.slice(end + 3) };
57
- }
58
-
59
41
  function getSectionTitle(filePath: string): string {
60
42
  const parts = filePath.split("/");
61
43
  if (parts.length > 1) {
@@ -75,7 +57,7 @@ function slugify(text: string): string {
75
57
  }
76
58
 
77
59
  function extractRecords(filePath: string, raw: string): SearchRecord[] {
78
- const { frontmatter, content } = parseFrontmatter(raw);
60
+ const { frontmatter, strippedContent: content } = extractFrontmatterWithContent<Frontmatter>(raw);
79
61
  const records: SearchRecord[] = [];
80
62
  const url = `/docs/${filePath}`;
81
63
  const lvl0 = getSectionTitle(filePath);
@@ -33,6 +33,35 @@ export async function getGitLastModified(filePath: string): Promise<string | nul
33
33
  }
34
34
  }
35
35
 
36
+ /** Batch git last modified dates for multiple files in a single spawn */
37
+ export async function getGitLastModifiedBatch(filePaths: string[]): Promise<Map<string, string>> {
38
+ const result = new Map<string, string>();
39
+ if (filePaths.length === 0) return result;
40
+
41
+ try {
42
+ const proc = Bun.spawn(
43
+ ["git", "log", "--format=%cI", "--name-only", "--diff-filter=ACMR", ...filePaths],
44
+ { stderr: "ignore" }
45
+ );
46
+ const text = await new Response(proc.stdout).text();
47
+ let currentDate = "";
48
+
49
+ for (const line of text.split("\n")) {
50
+ const trimmed = line.trim();
51
+ if (!trimmed) continue;
52
+ if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
53
+ currentDate = trimmed;
54
+ } else if (currentDate && !result.has(trimmed)) {
55
+ result.set(trimmed, currentDate);
56
+ }
57
+ }
58
+ } catch {
59
+ // fallback: return empty map, callers use frontmatter date
60
+ }
61
+
62
+ return result;
63
+ }
64
+
36
65
  const MIME_TYPES: Record<string, string> = {
37
66
  html: "text/html",
38
67
  css: "text/css",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/flame",
3
- "version": "1.0.0-beta.70",
3
+ "version": "1.0.0-beta.80",
4
4
  "description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
5
5
  "type": "module",
6
6
  "bin": {