Skip to content

Commit 134b3eb

Browse files
feat(docs): ToC: (#1591)
* feat(docs): ToC: - add copy-as-markdown button - add "Edit this page" button * refactor(docs): remove 'new' tags from sidebar items * style(toc): update button colors for improved accessibility
1 parent 213be8e commit 134b3eb

File tree

6 files changed

+170
-124
lines changed

6 files changed

+170
-124
lines changed

apps/web/components/toc.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"use client";
22

33
import type { Doc } from "contentlayer/generated";
4+
import { Check, Edit, FileCopy } from "flowbite-react-icons/outline";
5+
import { Github } from "flowbite-react-icons/solid";
46
import Link from "next/link";
57
import { useEffect, useState } from "react";
68
import Markdown from "react-markdown";
79
import { twMerge } from "tailwind-merge";
10+
import { convertMdxContentToMd } from "~/helpers/md";
811

912
export function ToC({ doc }: { doc: Doc }) {
1013
const [activeId, setActiveId] = useState<string>("");
@@ -89,6 +92,13 @@ export function ToC({ doc }: { doc: Doc }) {
8992
{doc.toc}
9093
</Markdown>
9194
</nav>
95+
<div className="ml-2.5 mt-4 flex flex-col items-start justify-between gap-4 border-t border-gray-200 pt-4 dark:border-gray-700">
96+
<CopyMarkdownButton
97+
// add title and description to the body as frontmatter
98+
content={`---\ntitle: ${doc.title}\ndescription: ${doc.description}\n---\n${doc.body.raw}`}
99+
/>
100+
<EditPageLink url={doc.url} />
101+
</div>
92102
</div>
93103
</div>
94104
</div>
@@ -111,3 +121,40 @@ function ToCLink({ href, children, isActive }: { href: string; children: React.R
111121
</Link>
112122
);
113123
}
124+
125+
function CopyMarkdownButton({ content }: { content: string }) {
126+
const [isCopied, setIsCopied] = useState(false);
127+
128+
function handleCopy() {
129+
setIsCopied(true);
130+
setTimeout(() => setIsCopied(false), 2000);
131+
navigator.clipboard.writeText(convertMdxContentToMd(content));
132+
}
133+
134+
const Icon = isCopied ? Check : FileCopy;
135+
136+
return (
137+
<button
138+
title="Copy as Markdown"
139+
className="flex items-center rounded-md text-sm font-medium text-gray-600 hover:text-gray-900 hover:underline dark:text-gray-400 dark:hover:text-gray-300"
140+
onClick={handleCopy}
141+
>
142+
<Icon className="me-1.5 size-4" />
143+
Copy as Markdown
144+
</button>
145+
);
146+
}
147+
148+
function EditPageLink({ url }: { url: string }) {
149+
return (
150+
<Link
151+
title="Edit this page"
152+
target="_blank"
153+
href={`https://github.com/themesberg/flowbite-react/tree/main/apps/web/content/docs/${url}.mdx`}
154+
className="flex items-center font-medium text-gray-600 hover:text-gray-900 hover:underline dark:text-gray-400 dark:hover:text-gray-300"
155+
>
156+
<Github className="me-1.5 size-4" />
157+
Edit this page
158+
</Link>
159+
);
160+
}

apps/web/data/docs-sidebar.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [
1515
items: [
1616
{ title: "Introduction", href: "/docs/getting-started/introduction" },
1717
{ title: "Quickstart", href: "/docs/getting-started/quickstart", tag: "updated" },
18-
{ title: "Compatibility", href: "/docs/getting-started/compatibility", tag: "new" },
18+
{ title: "Compatibility", href: "/docs/getting-started/compatibility" },
1919
{ title: "CLI", href: "/docs/getting-started/cli", tag: "updated" },
2020
{ title: "Editor Setup", href: "/docs/getting-started/editor-setup" },
21-
{ title: "AI Integration", href: "/docs/getting-started/ai-integration", tag: "new" },
21+
{ title: "AI Integration", href: "/docs/getting-started/ai-integration" },
2222
{ title: "Server Components", href: "/docs/getting-started/server-components" },
2323
{ title: "License", href: "/docs/getting-started/license" },
2424
{ title: "Changelog", href: "https://github.com/themesberg/flowbite-react/releases" },
@@ -30,38 +30,38 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [
3030
items: [
3131
{ title: "AdonisJS", href: "/docs/guides/adonisjs" },
3232
{ title: "Astro", href: "/docs/guides/astro" },
33-
{ title: "Blitz.js", href: "/docs/guides/blitzjs", tag: "new" },
34-
{ title: "Bun", href: "/docs/guides/bun", tag: "new" },
35-
{ title: "ESBuild", href: "/docs/guides/esbuild", tag: "new" },
36-
{ title: "Farm", href: "/docs/guides/farm", tag: "new" },
33+
{ title: "Blitz.js", href: "/docs/guides/blitzjs" },
34+
{ title: "Bun", href: "/docs/guides/bun" },
35+
{ title: "ESBuild", href: "/docs/guides/esbuild" },
36+
{ title: "Farm", href: "/docs/guides/farm" },
3737
{ title: "Gatsby", href: "/docs/guides/gatsby" },
3838
{ title: "Laravel", href: "/docs/guides/laravel" },
39-
{ title: "Meteor.js", href: "/docs/guides/meteorjs", tag: "new" },
40-
{ title: "Modern.js", href: "/docs/guides/modernjs", tag: "new" },
39+
{ title: "Meteor.js", href: "/docs/guides/meteorjs" },
40+
{ title: "Modern.js", href: "/docs/guides/modernjs" },
4141
{ title: "Next.js", href: "/docs/guides/nextjs" },
4242
{ title: "Parcel", href: "/docs/guides/parcel" },
43-
{ title: "React Router", href: "/docs/guides/react-router", tag: "new" },
44-
{ title: "React Server", href: "/docs/guides/react-server", tag: "new" },
43+
{ title: "React Router", href: "/docs/guides/react-router" },
44+
{ title: "React Server", href: "/docs/guides/react-server" },
4545
{ title: "RedwoodJS", href: "/docs/guides/redwoodjs" },
4646
{ title: "Remix", href: "/docs/guides/remix" },
47-
{ title: "Rsbuild", href: "/docs/guides/rsbuild", tag: "new" },
48-
{ title: "Rspack", href: "/docs/guides/rspack", tag: "new" },
49-
{ title: "TanStack Router", href: "/docs/guides/tanstack-router", tag: "new" },
50-
{ title: "TanStack Start", href: "/docs/guides/tanstack-start", tag: "new" },
51-
{ title: "Vike", href: "/docs/guides/vike", tag: "new" },
47+
{ title: "Rsbuild", href: "/docs/guides/rsbuild" },
48+
{ title: "Rspack", href: "/docs/guides/rspack" },
49+
{ title: "TanStack Router", href: "/docs/guides/tanstack-router" },
50+
{ title: "TanStack Start", href: "/docs/guides/tanstack-start" },
51+
{ title: "Vike", href: "/docs/guides/vike" },
5252
{ title: "Vite", href: "/docs/guides/vite" },
53-
{ title: "Waku", href: "/docs/guides/waku", tag: "new" },
54-
{ title: "Webpack", href: "/docs/guides/webpack", tag: "new" },
53+
{ title: "Waku", href: "/docs/guides/waku" },
54+
{ title: "Webpack", href: "/docs/guides/webpack" },
5555
],
5656
},
5757
{
5858
title: "customize",
5959
items: [
60-
{ title: "Colors", href: "/docs/customize/colors", tag: "new" },
61-
{ title: "Config", href: "/docs/customize/config", tag: "new" },
62-
{ title: "Custom Components", href: "/docs/customize/custom-components", tag: "new" },
60+
{ title: "Colors", href: "/docs/customize/colors" },
61+
{ title: "Config", href: "/docs/customize/config" },
62+
{ title: "Custom Components", href: "/docs/customize/custom-components" },
6363
{ title: "Dark Mode", href: "/docs/customize/dark-mode" },
64-
{ title: "Prefix", href: "/docs/customize/prefix", tag: "new" },
64+
{ title: "Prefix", href: "/docs/customize/prefix" },
6565
{ title: "Theme", href: "/docs/customize/theme", tag: "updated" },
6666
],
6767
},

apps/web/helpers/md.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { theme } from "flowbite-react";
2+
import type { FlowbiteTheme } from "flowbite-react/types";
3+
import type { CodeData } from "~/components/code-demo";
4+
import { GUIDES } from "~/components/quickstart/integration-guides";
5+
import * as examples from "~/examples";
6+
import { pick } from "~/helpers/pick";
7+
8+
/**
9+
* Converts MDX content to MD format
10+
*/
11+
export function convertMdxContentToMd(content: string, baseUrl: string = "https://flowbite-react.com"): string {
12+
let result = content;
13+
14+
// Convert frontmatter to MD format
15+
let title = "";
16+
let description = "";
17+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
18+
19+
if (frontmatterMatch) {
20+
const frontmatter = frontmatterMatch[1];
21+
const titleMatch = frontmatter.match(/title:\s*["']?(.*?)["']?\s*(\n|$)/);
22+
const descriptionMatch = frontmatter.match(/description:\s*["']?(.*?)["']?\s*(\n|$)/);
23+
24+
if (titleMatch) {
25+
title = titleMatch[1];
26+
}
27+
if (descriptionMatch) {
28+
description = descriptionMatch[1];
29+
}
30+
31+
result = result.replace(/^---\n[\s\S]*?\n---\n/, "");
32+
33+
let newHeader = "";
34+
if (title) {
35+
newHeader += `# ${title}\n\n`;
36+
}
37+
if (description) {
38+
newHeader += `> ${description}\n`;
39+
}
40+
41+
result = newHeader + result;
42+
}
43+
44+
// Process `Theme` component
45+
result = result.replace(/<Theme\s+name="([^"]+)"\s*\/>/g, (_, name: keyof FlowbiteTheme) => {
46+
if (theme[name]) {
47+
return "```json\n" + JSON.stringify(theme[name], null, 2) + "\n```";
48+
}
49+
50+
return `<Theme name="${name}" />`;
51+
});
52+
53+
// Process `Example` component
54+
result = result.replace(/<Example\s+name="([^"]+)"\s*\/>/g, (_, name) => {
55+
const codeData = pick<CodeData>(examples, name);
56+
57+
if (!codeData) {
58+
return `<Example name="${name}" />`;
59+
}
60+
61+
return formatCode(codeData);
62+
});
63+
64+
// Process `IntegrationGuides` component
65+
result = result.replace(/<IntegrationGuides\s*\/>/g, () => {
66+
let guidesContent = "";
67+
for (const guide of GUIDES) {
68+
guidesContent += `- [${guide.name}](${guide.slug})\n`;
69+
}
70+
return guidesContent;
71+
});
72+
73+
result = result
74+
.replace(/\]\(\/docs\//g, `](${baseUrl}/docs/`)
75+
.replace(/\]\(\/docs\/([^)]+)\)/g, `](${baseUrl}/docs/$1.md)`)
76+
.replace(/\]\((https?:\/\/flowbite-react\.com\/docs\/[^)]+)(?!\.md)\)/g, `]($1.md)`)
77+
.replace(/\]\(([^)]+)https:\/\/flowbite-react\.com\/docs\/([^)]+)\.md\)/g, `]($1$2.md)`);
78+
79+
return result;
80+
}
81+
82+
/**
83+
* Formats code examples
84+
*/
85+
function formatCode(data: CodeData): string {
86+
function getCodeContent(data: CodeData, variant: string = ""): CodeData["code"] {
87+
return data.type === "variant" ? data.code[variant || data.variant] : data.code;
88+
}
89+
90+
const code = getCodeContent(data); // TODO: Implement variant selection logic
91+
const codeItems = Array.isArray(code) ? code : [code];
92+
93+
return codeItems
94+
.map((item) => `\`\`\`${item.language}\n// ${item.fileName}.${item.language}\n${item.code}\`\`\``)
95+
.join("\n\n");
96+
}

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"contentlayer2": "0.5.3",
1919
"flowbite-react": "workspace:*",
20+
"flowbite-react-icons": "1.3.0",
2021
"react-icons": "5.2.1",
2122
"remark-gfm": "4.0.1",
2223
"tailwind-merge": "2.6.0"

apps/web/scripts/generate-llms.ts

Lines changed: 1 addition & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import path from "path";
2-
import { theme } from "flowbite-react";
3-
import type { FlowbiteTheme } from "flowbite-react/types";
4-
import type { CodeData } from "~/components/code-demo";
5-
import { GUIDES } from "~/components/quickstart/integration-guides";
6-
import * as examples from "~/examples";
7-
import { pick } from "~/helpers/pick";
2+
import { convertMdxContentToMd } from "~/helpers/md";
83
import { DOCS_SIDEBAR } from "../data/docs-sidebar";
94

105
const BASE_URL = "https://flowbite-react.com";
@@ -95,100 +90,4 @@ async function generateDocsFiles(): Promise<void> {
9590
}
9691
}
9792

98-
/**
99-
* Converts MDX content to MD format
100-
*/
101-
function convertMdxContentToMd(content: string): string {
102-
let result = content;
103-
104-
// Convert frontmatter to MD format
105-
let title = "";
106-
let description = "";
107-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
108-
109-
if (frontmatterMatch) {
110-
const frontmatter = frontmatterMatch[1];
111-
const titleMatch = frontmatter.match(/title:\s*["']?(.*?)["']?\s*(\n|$)/);
112-
const descriptionMatch = frontmatter.match(/description:\s*["']?(.*?)["']?\s*(\n|$)/);
113-
114-
if (titleMatch) {
115-
title = titleMatch[1];
116-
}
117-
if (descriptionMatch) {
118-
description = descriptionMatch[1];
119-
}
120-
121-
result = result.replace(/^---\n[\s\S]*?\n---\n/, "");
122-
123-
let newHeader = "";
124-
if (title) {
125-
newHeader += `# ${title}\n\n`;
126-
}
127-
if (description) {
128-
newHeader += `> ${description}\n`;
129-
}
130-
131-
result = newHeader + result;
132-
}
133-
134-
// Process `Theme` component
135-
result = result.replace(/<Theme\s+name="([^"]+)"\s*\/>/g, (_, name: keyof FlowbiteTheme) => {
136-
if (theme[name]) {
137-
return "```json\n" + JSON.stringify(theme[name], null, 2) + "\n```";
138-
}
139-
140-
return `<Theme name="${name}" />`;
141-
});
142-
143-
// Process `Example` component
144-
result = result.replace(/<Example\s+name="([^"]+)"\s*\/>/g, (_, name) => {
145-
const codeData = pick<CodeData>(examples, name);
146-
147-
if (!codeData) {
148-
return `<Example name="${name}" />`;
149-
}
150-
151-
return formatCode(codeData);
152-
});
153-
154-
// Process `IntegrationGuides` component
155-
result = result.replace(/<IntegrationGuides\s*\/>/g, () => {
156-
let guidesContent = "";
157-
for (const guide of GUIDES) {
158-
guidesContent += `- [${guide.name}](${guide.slug})\n`;
159-
}
160-
return guidesContent;
161-
});
162-
163-
// Transform relative and absolute links to properly point to markdown files
164-
// 1. Convert relative /docs/ links to include BASE_URL
165-
// 2. Add .md extension to doc links
166-
result = result
167-
.replace(/\]\(\/docs\//g, `](${BASE_URL}/docs/`)
168-
.replace(/\]\(\/docs\/([^)]+)\)/g, `](${BASE_URL}/docs/$1.md)`)
169-
.replace(/\]\((https?:\/\/flowbite-react\.com\/docs\/[^)]+)(?!\.md)\)/g, `]($1.md)`)
170-
.replace(
171-
/\]\(([^)]+)https:\/\/flowbite-react\.com\/docs\/([^)]+)\.md\)/g,
172-
`](https://flowbite-react.com/docs/$2.md)`,
173-
);
174-
175-
return result;
176-
}
177-
178-
/**
179-
* Formats code examples
180-
*/
181-
function formatCode(data: CodeData): string {
182-
function getCodeContent(data: CodeData, variant: string = ""): CodeData["code"] {
183-
return data.type === "variant" ? data.code[variant || data.variant] : data.code;
184-
}
185-
186-
const code = getCodeContent(data); // TODO: Implement variant selection logic
187-
const codeItems = Array.isArray(code) ? code : [code];
188-
189-
return codeItems
190-
.map((item) => `\`\`\`${item.language}\n// ${item.fileName}.${item.language}\n${item.code}\`\`\``)
191-
.join("\n\n");
192-
}
193-
19493
main();

bun.lock

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"dependencies": {
6262
"contentlayer2": "0.5.3",
6363
"flowbite-react": "workspace:*",
64+
"flowbite-react-icons": "1.3.0",
6465
"react-icons": "5.2.1",
6566
"remark-gfm": "4.0.1",
6667
"tailwind-merge": "2.6.0",
@@ -109,7 +110,7 @@
109110
},
110111
"packages/ui": {
111112
"name": "flowbite-react",
112-
"version": "0.11.5",
113+
"version": "0.11.9",
113114
"bin": {
114115
"flowbite-react": "./dist/cli/bin.js",
115116
},
@@ -2308,6 +2309,8 @@
23082309

23092310
"flowbite-react": ["flowbite-react@workspace:packages/ui"],
23102311

2312+
"flowbite-react-icons": ["[email protected]", "", { "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-gu8I6ETu/L23hwDnyaGc61qjZTKZM/CrmXc2x2Q6pqrqv8fyJsIGc1E/vbYuVELAnXa/L2CgRuGrsz/GMi7L2w=="],
2313+
23112314
"follow-redirects": ["[email protected]", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
23122315

23132316
"for-each": ["[email protected]", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],

0 commit comments

Comments
 (0)