@docubook/flame 1.0.0-beta.80 → 1.0.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.
- 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 +19 -4
- package/.docu/components/Sublink.tsx +6 -2
- package/.docu/components/Theme.tsx +1 -1
- package/.docu/components/registry.ts +21 -87
- package/.docu/node/build.ts +6 -2
- package/.docu/node/client.ts +1 -8
- package/.docu/node/html.ts +12 -0
- package/.docu/node/parse-tocs.ts +10 -0
- package/.docu/node/search-indexer.ts +17 -3
- package/.docu/node/server.ts +1 -13
- package/.docu/node/utils.ts +6 -4
- package/.docu/pages/docs/[[...slug]].tsx +6 -1
- package/.docu/styles/globals.css +8 -0
- package/README.md +2 -2
- package/package.json +8 -4
- package/template/docs/index.mdx +122 -16
- 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/index.ts +0 -21
- package/.docu/components/base/pagination/pagination-docs.tsx +0 -39
- package/.docu/components/base/pagination/pagination-numbers.tsx +0 -379
- package/.docu/components/base/pagination/types.ts +0 -129
- package/.docu/components/base/theme-controller.tsx +0 -358
- package/.docu/components/base/toggle.tsx +0 -172
package/template/docs/index.mdx
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
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
|
|
@@ -17,6 +32,8 @@ bun run dev
|
|
|
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
|
+
```markdown
|
|
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
|
+
```
|
|
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
|
|
102
192
|
```
|
|
103
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
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Then set environment variables:
|
|
203
|
+
|
|
204
|
+
```
|
|
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 };
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../node/utils";
|
|
4
|
-
import { X } from "lucide-react";
|
|
5
|
-
import { useState, type ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
type DrawerSide = "left" | "right";
|
|
8
|
-
|
|
9
|
-
interface DrawerProps {
|
|
10
|
-
id: string;
|
|
11
|
-
children: ReactNode;
|
|
12
|
-
defaultOpen?: boolean;
|
|
13
|
-
side?: DrawerSide;
|
|
14
|
-
breakpoint?: string;
|
|
15
|
-
withOverlay?: boolean;
|
|
16
|
-
overlayClassName?: string;
|
|
17
|
-
sideClassName?: string;
|
|
18
|
-
contentClassName?: string;
|
|
19
|
-
className?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export default function Drawer({
|
|
23
|
-
id,
|
|
24
|
-
children,
|
|
25
|
-
defaultOpen = false,
|
|
26
|
-
side = "left",
|
|
27
|
-
breakpoint = "lg",
|
|
28
|
-
withOverlay = true,
|
|
29
|
-
overlayClassName,
|
|
30
|
-
sideClassName,
|
|
31
|
-
contentClassName,
|
|
32
|
-
className,
|
|
33
|
-
}: DrawerProps) {
|
|
34
|
-
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
35
|
-
|
|
36
|
-
const toggleDrawer = () => setIsOpen(!isOpen);
|
|
37
|
-
const closeDrawer = () => setIsOpen(false);
|
|
38
|
-
|
|
39
|
-
const sidePositionClass = side === "right" ? "drawer-end" : "";
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div
|
|
43
|
-
className={cn(
|
|
44
|
-
"drawer",
|
|
45
|
-
breakpoint ? `${breakpoint}:drawer-open` : "",
|
|
46
|
-
sidePositionClass,
|
|
47
|
-
className
|
|
48
|
-
)}
|
|
49
|
-
>
|
|
50
|
-
<input
|
|
51
|
-
id={id}
|
|
52
|
-
type="checkbox"
|
|
53
|
-
className="drawer-toggle"
|
|
54
|
-
checked={isOpen}
|
|
55
|
-
onChange={toggleDrawer}
|
|
56
|
-
aria-label="Toggle drawer"
|
|
57
|
-
/>
|
|
58
|
-
|
|
59
|
-
<div className={cn("drawer-content flex flex-col", contentClassName)}>
|
|
60
|
-
{typeof children === "function"
|
|
61
|
-
? (
|
|
62
|
-
children as (props: {
|
|
63
|
-
open: boolean;
|
|
64
|
-
toggle: () => void;
|
|
65
|
-
close: () => void;
|
|
66
|
-
}) => ReactNode
|
|
67
|
-
)({ open: isOpen, toggle: toggleDrawer, close: closeDrawer })
|
|
68
|
-
: children}
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<div className={cn("drawer-side", sideClassName)}>
|
|
72
|
-
{withOverlay && (
|
|
73
|
-
<label
|
|
74
|
-
htmlFor={id}
|
|
75
|
-
aria-label="Close sidebar"
|
|
76
|
-
className={cn("drawer-overlay", overlayClassName)}
|
|
77
|
-
onClick={closeDrawer}
|
|
78
|
-
/>
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
<div className="bg-base-200 min-h-full w-80 p-4">
|
|
82
|
-
<div className="mb-4 flex items-center justify-between">
|
|
83
|
-
<span className="text-lg font-semibold">Menu</span>
|
|
84
|
-
<button
|
|
85
|
-
onClick={closeDrawer}
|
|
86
|
-
className="btn btn-ghost btn-sm btn-circle"
|
|
87
|
-
aria-label="Close drawer"
|
|
88
|
-
>
|
|
89
|
-
<X className="h-5 w-5" />
|
|
90
|
-
</button>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function useDrawerState(initialState = false) {
|
|
99
|
-
const [isOpen, setIsOpen] = useState(initialState);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
isOpen,
|
|
103
|
-
open: () => setIsOpen(true),
|
|
104
|
-
close: () => setIsOpen(false),
|
|
105
|
-
toggle: () => setIsOpen((prev) => !prev),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface DrawerTriggerProps {
|
|
110
|
-
drawerId: string;
|
|
111
|
-
children: ReactNode;
|
|
112
|
-
className?: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function DrawerTrigger({ drawerId, children, className }: DrawerTriggerProps) {
|
|
116
|
-
return (
|
|
117
|
-
<label htmlFor={drawerId} className={cn("label cursor-pointer", className)}>
|
|
118
|
-
{children}
|
|
119
|
-
</label>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
interface DrawerContentProps {
|
|
124
|
-
children: ReactNode;
|
|
125
|
-
className?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function DrawerContent({ children, className }: DrawerContentProps) {
|
|
129
|
-
return <div className={cn("drawer-content", className)}>{children}</div>;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface DrawerSidePanelProps {
|
|
133
|
-
id: string;
|
|
134
|
-
children: ReactNode;
|
|
135
|
-
side?: DrawerSide;
|
|
136
|
-
withOverlay?: boolean;
|
|
137
|
-
overlayClassName?: string;
|
|
138
|
-
className?: string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function DrawerSidePanel({
|
|
142
|
-
id,
|
|
143
|
-
children,
|
|
144
|
-
side = "left",
|
|
145
|
-
withOverlay = true,
|
|
146
|
-
overlayClassName,
|
|
147
|
-
className,
|
|
148
|
-
}: DrawerSidePanelProps) {
|
|
149
|
-
return (
|
|
150
|
-
<div className={cn("drawer-side", side === "right" ? "drawer-end" : "", className)}>
|
|
151
|
-
{withOverlay && (
|
|
152
|
-
<label
|
|
153
|
-
htmlFor={id}
|
|
154
|
-
aria-label="Close sidebar"
|
|
155
|
-
className={cn("drawer-overlay", overlayClassName)}
|
|
156
|
-
/>
|
|
157
|
-
)}
|
|
158
|
-
{children}
|
|
159
|
-
</div>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export { Drawer };
|
|
164
|
-
export type { DrawerProps, DrawerSide };
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { type HTMLAttributes, type ReactNode, forwardRef, useRef, useEffect } from "react";
|
|
4
|
-
import { cn } from "../../node/utils";
|
|
5
|
-
|
|
6
|
-
export interface DropdownProps extends HTMLAttributes<HTMLDetailsElement> {
|
|
7
|
-
align?: "start" | "end";
|
|
8
|
-
disabled?: boolean;
|
|
9
|
-
trigger?: ReactNode;
|
|
10
|
-
children?: ReactNode;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const Dropdown = forwardRef<HTMLDetailsElement, DropdownProps>(
|
|
14
|
-
({ className, align = "start", disabled = false, trigger, children, ...props }, ref) => {
|
|
15
|
-
const detailsRef = useRef<HTMLDetailsElement>(null);
|
|
16
|
-
|
|
17
|
-
// Close on click outside
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
const handler = (e: MouseEvent) => {
|
|
20
|
-
if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) {
|
|
21
|
-
detailsRef.current.open = false;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
document.addEventListener("click", handler);
|
|
25
|
-
return () => document.removeEventListener("click", handler);
|
|
26
|
-
}, []);
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<details
|
|
30
|
-
ref={(el) => {
|
|
31
|
-
(detailsRef as React.MutableRefObject<HTMLDetailsElement | null>).current = el;
|
|
32
|
-
if (typeof ref === "function") ref(el);
|
|
33
|
-
else if (ref) ref.current = el;
|
|
34
|
-
}}
|
|
35
|
-
className={cn("relative", disabled && "pointer-events-none opacity-50", className)}
|
|
36
|
-
{...props}
|
|
37
|
-
>
|
|
38
|
-
<summary className="cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
|
39
|
-
{trigger}
|
|
40
|
-
</summary>
|
|
41
|
-
<ul
|
|
42
|
-
className={cn(
|
|
43
|
-
"bg-base-100 rounded-box border-base-300 absolute z-50 mt-1 w-full min-w-[200px] space-y-1 border p-2 shadow-lg",
|
|
44
|
-
align === "end" ? "right-0" : "left-0"
|
|
45
|
-
)}
|
|
46
|
-
>
|
|
47
|
-
{children}
|
|
48
|
-
</ul>
|
|
49
|
-
</details>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
);
|
|
53
|
-
Dropdown.displayName = "Dropdown";
|
|
54
|
-
|
|
55
|
-
export interface DropdownItemProps extends HTMLAttributes<HTMLLIElement> {
|
|
56
|
-
children?: ReactNode;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const DropdownItem = forwardRef<HTMLLIElement, DropdownItemProps>(
|
|
60
|
-
({ className, children, ...props }, ref) => (
|
|
61
|
-
<li ref={ref} className={cn(className)} role="menuitem" {...props}>
|
|
62
|
-
{children}
|
|
63
|
-
</li>
|
|
64
|
-
)
|
|
65
|
-
);
|
|
66
|
-
DropdownItem.displayName = "DropdownItem";
|
|
67
|
-
|
|
68
|
-
export const DropdownLink = forwardRef<
|
|
69
|
-
HTMLAnchorElement,
|
|
70
|
-
HTMLAttributes<HTMLAnchorElement> & { href?: string }
|
|
71
|
-
>(({ className, children, href, ...props }, ref) => (
|
|
72
|
-
<li role="menuitem">
|
|
73
|
-
<a ref={ref} href={href} className={cn(className)} {...props}>
|
|
74
|
-
{children}
|
|
75
|
-
</a>
|
|
76
|
-
</li>
|
|
77
|
-
));
|
|
78
|
-
DropdownLink.displayName = "DropdownLink";
|
|
79
|
-
|
|
80
|
-
export const DropdownLabel = forwardRef<HTMLLIElement, HTMLAttributes<HTMLLIElement>>(
|
|
81
|
-
({ className, children, ...props }, ref) => (
|
|
82
|
-
<li ref={ref} className={cn("menu-title", className)} {...props}>
|
|
83
|
-
{children}
|
|
84
|
-
</li>
|
|
85
|
-
)
|
|
86
|
-
);
|
|
87
|
-
DropdownLabel.displayName = "DropdownLabel";
|
|
88
|
-
|
|
89
|
-
export const DropdownDivider = forwardRef<HTMLLIElement, HTMLAttributes<HTMLLIElement>>(
|
|
90
|
-
({ className, ...props }, ref) => (
|
|
91
|
-
<li ref={ref} className={cn("border-base-200 my-1 border-t", className)} {...props} />
|
|
92
|
-
)
|
|
93
|
-
);
|
|
94
|
-
DropdownDivider.displayName = "DropdownDivider";
|