@docubook/flame 1.0.0-beta.70 → 1.0.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/Anchor.tsx +1 -1
- package/.docu/components/Breadcrumb.tsx +6 -1
- package/.docu/components/Context.tsx +2 -1
- package/.docu/components/Navbar.tsx +2 -2
- package/.docu/components/Pagination.tsx +15 -2
- package/.docu/components/Search.tsx +4 -10
- package/.docu/components/Sidebar.tsx +15 -4
- package/.docu/components/Theme.tsx +1 -1
- package/.docu/components/registry.ts +21 -87
- package/.docu/node/build.ts +29 -7
- package/.docu/node/client.ts +1 -8
- package/.docu/node/html.ts +12 -0
- package/.docu/node/mdx.ts +13 -3
- package/.docu/node/parse-tocs.ts +10 -0
- package/.docu/node/search-indexer.ts +18 -22
- package/.docu/node/server.ts +1 -13
- package/.docu/node/utils.ts +33 -2
- package/.docu/pages/docs/[[...slug]].tsx +6 -1
- package/.docu/styles/globals.css +8 -0
- package/README.md +2 -2
- package/package.json +9 -5
- package/template/docs/index.mdx +123 -17
- package/.docu/components/base/breadcrumbs.tsx +0 -64
- package/.docu/components/base/collapse.tsx +0 -186
- package/.docu/components/base/drawer.tsx +0 -164
- package/.docu/components/base/dropdown.tsx +0 -94
- package/.docu/components/base/input.tsx +0 -53
- package/.docu/components/base/kbd.tsx +0 -51
- package/.docu/components/base/modal.tsx +0 -56
- package/.docu/components/base/navbar.tsx +0 -132
- package/.docu/components/base/pagination.tsx +0 -548
- package/.docu/components/base/theme-controller.tsx +0 -358
- package/.docu/components/base/toggle.tsx +0 -172
package/.docu/node/utils.ts
CHANGED
|
@@ -7,7 +7,8 @@ export function isExternalUrl(url: string): boolean {
|
|
|
7
7
|
export function getPath(url: string): string {
|
|
8
8
|
try {
|
|
9
9
|
return new URL(url).pathname;
|
|
10
|
-
} catch {
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.error("Failed to parse URL", url, err);
|
|
11
12
|
return url;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -28,11 +29,41 @@ export async function getGitLastModified(filePath: string): Promise<string | nul
|
|
|
28
29
|
const text = await new Response(proc.stdout).text();
|
|
29
30
|
const date = text.trim();
|
|
30
31
|
return date || null;
|
|
31
|
-
} catch {
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error("Failed to get git last modified for", filePath, err);
|
|
32
34
|
return null;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/** Batch git last modified dates for multiple files in a single spawn */
|
|
39
|
+
export async function getGitLastModifiedBatch(filePaths: string[]): Promise<Map<string, string>> {
|
|
40
|
+
const result = new Map<string, string>();
|
|
41
|
+
if (filePaths.length === 0) return result;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const proc = Bun.spawn(
|
|
45
|
+
["git", "log", "--format=%cI", "--name-only", "--diff-filter=ACMR", ...filePaths],
|
|
46
|
+
{ stderr: "ignore" }
|
|
47
|
+
);
|
|
48
|
+
const text = await new Response(proc.stdout).text();
|
|
49
|
+
let currentDate = "";
|
|
50
|
+
|
|
51
|
+
for (const line of text.split("\n")) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed) continue;
|
|
54
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
|
|
55
|
+
currentDate = trimmed;
|
|
56
|
+
} else if (currentDate && !result.has(trimmed)) {
|
|
57
|
+
result.set(trimmed, currentDate);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("Failed to get git last modified batch for", filePaths, err);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
const MIME_TYPES: Record<string, string> = {
|
|
37
68
|
html: "text/html",
|
|
38
69
|
css: "text/css",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
3
|
import DocsBreadcrumb from "../../components/Breadcrumb";
|
|
3
4
|
import Pagination from "../../components/Pagination";
|
|
4
5
|
import { Typography } from "../../components/Typography";
|
|
@@ -72,7 +73,11 @@ export default function DocsPage({
|
|
|
72
73
|
</p>
|
|
73
74
|
)}
|
|
74
75
|
</div>
|
|
75
|
-
<Pagination
|
|
76
|
+
<Pagination
|
|
77
|
+
pathname={pathname}
|
|
78
|
+
prevIcon={<ChevronLeft className="h-3 w-3" />}
|
|
79
|
+
nextIcon={<ChevronRight className="h-3 w-3" />}
|
|
80
|
+
/>
|
|
76
81
|
<Footer />
|
|
77
82
|
</Typography>
|
|
78
83
|
</div>
|
package/.docu/styles/globals.css
CHANGED
|
@@ -126,6 +126,14 @@
|
|
|
126
126
|
background-color 0.2s ease,
|
|
127
127
|
color 0.2s ease;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/* Remove default summary marker from details elements */
|
|
131
|
+
summary::-webkit-details-marker {
|
|
132
|
+
display: none;
|
|
133
|
+
}
|
|
134
|
+
summary::marker {
|
|
135
|
+
display: none;
|
|
136
|
+
}
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
/* Code block utilities */
|
package/README.md
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
mkdir my-docs && cd my-docs
|
|
19
|
-
bun add @docubook/flame@
|
|
19
|
+
bun add @docubook/flame@rc
|
|
20
20
|
bunx flame init
|
|
21
21
|
bun run dev
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
>
|
|
24
|
+
> **Lightweight** — 📦 ~57 kB packed, ~207 kB unpacked. No bloat, just fire.
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.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": {
|
|
@@ -30,12 +30,15 @@
|
|
|
30
30
|
"static-site",
|
|
31
31
|
"daisyui",
|
|
32
32
|
"bun",
|
|
33
|
-
"docs framework"
|
|
33
|
+
"docs framework",
|
|
34
|
+
"react",
|
|
35
|
+
"react-dom"
|
|
34
36
|
],
|
|
35
37
|
"homepage": "https://docubook.pro/",
|
|
36
38
|
"repository": {
|
|
37
39
|
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/DocuBook/docubook.git"
|
|
40
|
+
"url": "git+https://github.com/DocuBook/docubook.git",
|
|
41
|
+
"directory": "packages/flame"
|
|
39
42
|
},
|
|
40
43
|
"author": "wildan.nrs",
|
|
41
44
|
"author-url": "https://wildan.dev",
|
|
@@ -48,8 +51,9 @@
|
|
|
48
51
|
"lucide-react": "^1.14.0",
|
|
49
52
|
"react": "^19.0.0",
|
|
50
53
|
"react-dom": "^19.0.0",
|
|
51
|
-
"@docubook/
|
|
52
|
-
"@docubook/
|
|
54
|
+
"@docubook/mdx-content": "^3.2.1",
|
|
55
|
+
"@docubook/ui-react": "^0.1.2",
|
|
56
|
+
"@docubook/core": "^1.7.0"
|
|
53
57
|
},
|
|
54
58
|
"peerDependencies": {
|
|
55
59
|
"@sentry/bun": "^9.0.0"
|
package/template/docs/index.mdx
CHANGED
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: DocuBook Flame
|
|
3
|
-
description:
|
|
2
|
+
title: DocuBook Flame 🔥
|
|
3
|
+
description: Fast as flame — a Bun-native framework for modern documentation experiences.
|
|
4
4
|
date: 2026-05-24
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
**@docubook/flame** is a lightweight runtime for building documentation websites using React, MDX, and filesystem-based routing — all running on Bun.
|
|
8
8
|
|
|
9
|
+
No heavy abstractions. No complex tooling. Just a minimal layer between your content and the browser.
|
|
10
|
+
|
|
11
|
+
> **Lightweight** — 📦 ~57 kB packed, ~207 kB unpacked. No bloat, just fire.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Bun-native** — instant startup, native TypeScript, fast builds
|
|
16
|
+
- **React-first** — JSX/TSX, hooks, component composition
|
|
17
|
+
- **MDX content** — write Markdown with embedded React components
|
|
18
|
+
- **Filesystem routing** — auto-detect routes from `docs/` folder
|
|
19
|
+
- **Lightweight SSR** — React server-side rendering without a heavy framework
|
|
20
|
+
- **Client hydration** — interactive islands for sidebar, TOC, and MDX components
|
|
21
|
+
- **HMR** — instant reload on docs changes during development
|
|
22
|
+
- **Static build** — pre-render all pages to static HTML for deployment
|
|
23
|
+
|
|
9
24
|
## Quick Start
|
|
10
25
|
|
|
11
26
|
```bash
|
|
12
27
|
mkdir my-docs && cd my-docs
|
|
13
|
-
bun add @docubook/flame
|
|
28
|
+
bun add @docubook/flame@rc
|
|
14
29
|
bunx flame init
|
|
15
30
|
bun run dev
|
|
16
31
|
```
|
|
17
32
|
|
|
18
33
|
## Project Structure
|
|
19
34
|
|
|
35
|
+
After `flame init`, your project looks like:
|
|
36
|
+
|
|
20
37
|
```
|
|
21
38
|
my-docs/
|
|
22
39
|
├── docs/ # Your MDX content
|
|
@@ -24,17 +41,17 @@ my-docs/
|
|
|
24
41
|
├── docu.json # Site configuration (navbar, routes, meta)
|
|
25
42
|
├── package.json # Dependencies
|
|
26
43
|
└── .docu/
|
|
27
|
-
└── dist/ # Build output (after bun run build)
|
|
44
|
+
└── dist/ # Build output (after `bun run build`)
|
|
28
45
|
```
|
|
29
46
|
|
|
30
47
|
## Commands
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
```bash
|
|
50
|
+
bun run dev # Start dev server with HMR
|
|
51
|
+
bun run build # Static build to .docu/dist/
|
|
52
|
+
bun run preview # Serve built output locally
|
|
53
|
+
bun run deploy # Build + prepare for GitHub Pages
|
|
54
|
+
```
|
|
38
55
|
|
|
39
56
|
## Configuration
|
|
40
57
|
|
|
@@ -57,7 +74,34 @@ my-docs/
|
|
|
57
74
|
}
|
|
58
75
|
```
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
### Routes
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
To define navigation manually, populate the `routes` array:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"routes": [
|
|
86
|
+
{
|
|
87
|
+
"title": "Getting Started",
|
|
88
|
+
"href": "/getting-started",
|
|
89
|
+
"noLink": true,
|
|
90
|
+
"context": {
|
|
91
|
+
"icon": "BookOpen",
|
|
92
|
+
"title": "Guides",
|
|
93
|
+
"description": "Set up your Documentation"
|
|
94
|
+
},
|
|
95
|
+
"items": [
|
|
96
|
+
{ "title": "Introduction", "href": "/introduction" },
|
|
97
|
+
{ "title": "Installation", "href": "/installation" }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> Manual routes take priority — if `routes` has entries, folder scanning is skipped entirely.
|
|
61
105
|
|
|
62
106
|
## Routing
|
|
63
107
|
|
|
@@ -72,6 +116,45 @@ docs/
|
|
|
72
116
|
└── card.mdx → /docs/components/card
|
|
73
117
|
```
|
|
74
118
|
|
|
119
|
+
## Assets
|
|
120
|
+
|
|
121
|
+
Place images and static files in `docs/assets/`. They are copied to the build output and accessible at `/docs/assets/`.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
docs/
|
|
125
|
+
├── assets/
|
|
126
|
+
│ └── images/
|
|
127
|
+
│ ├── logo.svg
|
|
128
|
+
│ └── screenshot.png
|
|
129
|
+
└── getting-started/
|
|
130
|
+
└── introduction.mdx
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Reference in MDX:
|
|
134
|
+
|
|
135
|
+
```mdx
|
|
136
|
+

|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> The `docs/assets/` directory is excluded from route scanning — files inside it won't appear in the sidebar.
|
|
140
|
+
|
|
141
|
+
## Architecture
|
|
142
|
+
|
|
143
|
+
- **Bun** — runtime, bundler, file watcher
|
|
144
|
+
- **React + React DOM** — rendering (SSR + client hydration)
|
|
145
|
+
- **@docubook/core** — MDX compilation, rehype/remark plugins
|
|
146
|
+
- **@docubook/mdx-content** — pre-built MDX components
|
|
147
|
+
- **Tailwind CSS + daisyUI** — styling
|
|
148
|
+
|
|
149
|
+
## Comparison
|
|
150
|
+
|
|
151
|
+
| Framework | Runtime | UI | Approach |
|
|
152
|
+
| ------------------- | ------- | --------- | --------------------------- |
|
|
153
|
+
| Docusaurus | Node.js | React | Full-featured, plugin-heavy |
|
|
154
|
+
| VitePress | Node.js | Vue | Lightweight, Vue-only |
|
|
155
|
+
| Nextra | Node.js | React | Next.js-based |
|
|
156
|
+
| **@docubook/flame** | **Bun** | **React** | **Minimal, Bun-native SSR** |
|
|
157
|
+
|
|
75
158
|
## Deployment
|
|
76
159
|
|
|
77
160
|
### GitHub Pages
|
|
@@ -80,27 +163,50 @@ docs/
|
|
|
80
163
|
bun run deploy
|
|
81
164
|
```
|
|
82
165
|
|
|
83
|
-
This will
|
|
166
|
+
This will:
|
|
167
|
+
|
|
168
|
+
1. Run production build → output to `.docu/dist/`
|
|
169
|
+
2. Add `.nojekyll` file
|
|
170
|
+
3. Generate `.github/workflows/deploy.yml` (first run only)
|
|
84
171
|
|
|
85
172
|
Then push to GitHub and enable Pages: **Settings → Pages → Source: GitHub Actions**
|
|
86
173
|
|
|
87
|
-
### Other Hosts
|
|
174
|
+
### Manual / Other Hosts
|
|
88
175
|
|
|
89
176
|
```bash
|
|
90
177
|
bun run build
|
|
91
178
|
```
|
|
92
179
|
|
|
93
|
-
Upload `.docu/dist/` to any static hosting (Netlify, Cloudflare Pages, Vercel, S3, etc).
|
|
180
|
+
Upload the contents of `.docu/dist/` to any static hosting (Netlify, Cloudflare Pages, Vercel, S3, etc).
|
|
94
181
|
|
|
95
182
|
## Environment Variables
|
|
96
183
|
|
|
97
|
-
Copy `.env.example` to `.env
|
|
184
|
+
Copy `.env.example` to `.env` to customize:
|
|
98
185
|
|
|
99
|
-
```
|
|
186
|
+
```env
|
|
187
|
+
# Server port (default: 3000)
|
|
100
188
|
PORT=3000
|
|
101
|
-
|
|
189
|
+
|
|
190
|
+
# Error Monitoring (optional)
|
|
191
|
+
SENTRY_DSN=https://your-dsn@sentry.io/project-id
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Error Monitoring (Optional)
|
|
195
|
+
|
|
196
|
+
Flame has built-in [Sentry](https://sentry.io) support for error tracking. To enable:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
bun add @sentry/bun
|
|
102
200
|
```
|
|
103
201
|
|
|
202
|
+
Then set environment variables:
|
|
203
|
+
|
|
204
|
+
```env
|
|
205
|
+
SENTRY_DSN=https://your-dsn@sentry.io/project-id
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Errors during dev server and build will be automatically captured. No configuration needed beyond the DSN.
|
|
209
|
+
|
|
104
210
|
## Requirements
|
|
105
211
|
|
|
106
212
|
- [Bun](https://bun.sh) >= 1.1.0
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export interface BreadcrumbProps {
|
|
4
|
-
children: ReactNode;
|
|
5
|
-
className?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface BreadcrumbItemProps {
|
|
9
|
-
children: ReactNode;
|
|
10
|
-
className?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface BreadcrumbLinkProps {
|
|
14
|
-
href?: string;
|
|
15
|
-
children: ReactNode;
|
|
16
|
-
className?: string;
|
|
17
|
-
onClick?: () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function Breadcrumb({ children, className = "" }: BreadcrumbProps) {
|
|
21
|
-
return (
|
|
22
|
-
<div className={`breadcrumbs text-sm ${className}`}>
|
|
23
|
-
<ul>{children}</ul>
|
|
24
|
-
</div>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function BreadcrumbItem({ children, className = "" }: BreadcrumbItemProps) {
|
|
29
|
-
return <li className={className}>{children}</li>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function BreadcrumbLink({ href, children, className = "", onClick }: BreadcrumbLinkProps) {
|
|
33
|
-
const baseClass = "link link-hover";
|
|
34
|
-
|
|
35
|
-
if (href) {
|
|
36
|
-
return (
|
|
37
|
-
<a href={href} className={`${baseClass} ${className}`}>
|
|
38
|
-
{children}
|
|
39
|
-
</a>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (onClick) {
|
|
44
|
-
return (
|
|
45
|
-
<button type="button" onClick={onClick} className={`${baseClass} ${className}`}>
|
|
46
|
-
{children}
|
|
47
|
-
</button>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return <span className={className}>{children}</span>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function BreadcrumbPage({ children, className = "" }: BreadcrumbItemProps) {
|
|
55
|
-
return <span className={className}>{children}</span>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function BreadcrumbSeparator({ className = "" }: { className?: string }) {
|
|
59
|
-
return <li className={className}></li>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function BreadcrumbList({ children }: BreadcrumbItemProps) {
|
|
63
|
-
return <>{children}</>;
|
|
64
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../node/utils";
|
|
4
|
-
import { ChevronDown, ChevronRight, Plus, Minus } from "lucide-react";
|
|
5
|
-
import { useState, type ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
type CollapseVariant = "arrow" | "plus";
|
|
8
|
-
|
|
9
|
-
interface CollapseProps {
|
|
10
|
-
title: ReactNode;
|
|
11
|
-
children: ReactNode;
|
|
12
|
-
defaultOpen?: boolean;
|
|
13
|
-
onOpenChange?: (open: boolean) => void;
|
|
14
|
-
variant?: CollapseVariant;
|
|
15
|
-
className?: string;
|
|
16
|
-
titleClassName?: string;
|
|
17
|
-
contentClassName?: string;
|
|
18
|
-
disabled?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export default function Collapse({
|
|
22
|
-
title,
|
|
23
|
-
children,
|
|
24
|
-
defaultOpen = false,
|
|
25
|
-
onOpenChange,
|
|
26
|
-
variant = "arrow",
|
|
27
|
-
className,
|
|
28
|
-
titleClassName,
|
|
29
|
-
contentClassName,
|
|
30
|
-
disabled = false,
|
|
31
|
-
}: CollapseProps) {
|
|
32
|
-
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
33
|
-
|
|
34
|
-
const handleToggle = () => {
|
|
35
|
-
if (disabled) return;
|
|
36
|
-
const newState = !isOpen;
|
|
37
|
-
setIsOpen(newState);
|
|
38
|
-
onOpenChange?.(newState);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const Icon = variant === "arrow" ? (isOpen ? ChevronDown : ChevronRight) : isOpen ? Minus : Plus;
|
|
42
|
-
|
|
43
|
-
const variantClass = variant === "arrow" ? "collapse-arrow" : "collapse-plus";
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div
|
|
47
|
-
className={cn(
|
|
48
|
-
"bg-base-100 border-base-200 collapse rounded-lg border",
|
|
49
|
-
isOpen && "collapse-open",
|
|
50
|
-
!isOpen && "collapse-close",
|
|
51
|
-
variantClass,
|
|
52
|
-
disabled && "cursor-not-allowed opacity-50",
|
|
53
|
-
className
|
|
54
|
-
)}
|
|
55
|
-
>
|
|
56
|
-
<input
|
|
57
|
-
type="checkbox"
|
|
58
|
-
checked={isOpen}
|
|
59
|
-
onChange={handleToggle}
|
|
60
|
-
disabled={disabled}
|
|
61
|
-
aria-expanded={isOpen}
|
|
62
|
-
/>
|
|
63
|
-
<div
|
|
64
|
-
className={cn(
|
|
65
|
-
"collapse-title text-base-content cursor-pointer font-medium",
|
|
66
|
-
titleClassName
|
|
67
|
-
)}
|
|
68
|
-
onClick={handleToggle}
|
|
69
|
-
role="button"
|
|
70
|
-
tabIndex={disabled ? -1 : 0}
|
|
71
|
-
onKeyDown={(e) => {
|
|
72
|
-
if (!disabled && (e.key === "Enter" || e.key === " ")) {
|
|
73
|
-
e.preventDefault();
|
|
74
|
-
setIsOpen(!isOpen);
|
|
75
|
-
}
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
<div className="flex items-center justify-between pr-8">
|
|
79
|
-
<span className="flex-1">{title}</span>
|
|
80
|
-
<Icon
|
|
81
|
-
className={cn(
|
|
82
|
-
"text-base-content/60 h-4 w-4 transition-transform duration-200",
|
|
83
|
-
isOpen && "rotate-180"
|
|
84
|
-
)}
|
|
85
|
-
aria-hidden="true"
|
|
86
|
-
/>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
<div className={cn("collapse-content text-base-content/80", contentClassName)}>
|
|
90
|
-
{children}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface AccordionItem {
|
|
97
|
-
id: string;
|
|
98
|
-
title: ReactNode;
|
|
99
|
-
content: ReactNode;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface AccordionProps {
|
|
103
|
-
items: AccordionItem[];
|
|
104
|
-
defaultOpen?: string;
|
|
105
|
-
variant?: CollapseVariant;
|
|
106
|
-
className?: string;
|
|
107
|
-
allowMultiple?: boolean;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function Accordion({
|
|
111
|
-
items,
|
|
112
|
-
defaultOpen,
|
|
113
|
-
variant = "arrow",
|
|
114
|
-
className,
|
|
115
|
-
allowMultiple = false,
|
|
116
|
-
}: AccordionProps) {
|
|
117
|
-
const [openIds, setOpenIds] = useState<Set<string>>(
|
|
118
|
-
defaultOpen ? new Set([defaultOpen]) : new Set()
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const toggle = (id: string) => {
|
|
122
|
-
setOpenIds((prev) => {
|
|
123
|
-
const next = new Set(prev);
|
|
124
|
-
if (next.has(id)) {
|
|
125
|
-
next.delete(id);
|
|
126
|
-
} else {
|
|
127
|
-
if (!allowMultiple) {
|
|
128
|
-
next.clear();
|
|
129
|
-
}
|
|
130
|
-
next.add(id);
|
|
131
|
-
}
|
|
132
|
-
return next;
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<div className={cn("flex flex-col gap-3", className)}>
|
|
138
|
-
{items.map((item) => {
|
|
139
|
-
const isOpen = openIds.has(item.id);
|
|
140
|
-
const Icon =
|
|
141
|
-
variant === "arrow" ? (isOpen ? ChevronDown : ChevronRight) : isOpen ? Minus : Plus;
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<div
|
|
145
|
-
key={item.id}
|
|
146
|
-
className={cn(
|
|
147
|
-
"bg-base-100 border-base-200 collapse rounded-lg border",
|
|
148
|
-
isOpen && "collapse-open",
|
|
149
|
-
!isOpen && "collapse-close",
|
|
150
|
-
variant === "arrow" ? "collapse-arrow" : "collapse-plus"
|
|
151
|
-
)}
|
|
152
|
-
>
|
|
153
|
-
<input type="checkbox" checked={isOpen} onChange={() => toggle(item.id)} />
|
|
154
|
-
<div
|
|
155
|
-
className="collapse-title text-base-content cursor-pointer pr-8 font-medium"
|
|
156
|
-
onClick={() => toggle(item.id)}
|
|
157
|
-
role="button"
|
|
158
|
-
tabIndex={0}
|
|
159
|
-
onKeyDown={(e) => {
|
|
160
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
toggle(item.id);
|
|
163
|
-
}
|
|
164
|
-
}}
|
|
165
|
-
>
|
|
166
|
-
<div className="flex items-center justify-between">
|
|
167
|
-
<span className="flex-1">{item.title}</span>
|
|
168
|
-
<Icon
|
|
169
|
-
className={cn(
|
|
170
|
-
"text-base-content/60 h-4 w-4 transition-transform duration-200",
|
|
171
|
-
isOpen && "rotate-180"
|
|
172
|
-
)}
|
|
173
|
-
aria-hidden="true"
|
|
174
|
-
/>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
<div className="collapse-content text-base-content/80">{item.content}</div>
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
180
|
-
})}
|
|
181
|
-
</div>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export { Collapse };
|
|
186
|
-
export type { CollapseProps, AccordionItem, AccordionProps };
|