react-embed-docs 0.1.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/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/client/components/Breadcrumbs.d.ts +21 -0
- package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/client/components/Breadcrumbs.js +123 -0
- package/dist/client/components/DocsLayout.d.ts +20 -0
- package/dist/client/components/DocsLayout.d.ts.map +1 -0
- package/dist/client/components/DocsLayout.js +387 -0
- package/dist/client/components/DocumentContent.d.ts +5 -0
- package/dist/client/components/DocumentContent.d.ts.map +1 -0
- package/dist/client/components/DocumentContent.js +15 -0
- package/dist/client/components/DocumentEdit.d.ts +6 -0
- package/dist/client/components/DocumentEdit.d.ts.map +1 -0
- package/dist/client/components/DocumentEdit.js +153 -0
- package/dist/client/components/DocumentList.d.ts +5 -0
- package/dist/client/components/DocumentList.d.ts.map +1 -0
- package/dist/client/components/DocumentList.js +39 -0
- package/dist/client/components/DocumentProvider.d.ts +42 -0
- package/dist/client/components/DocumentProvider.d.ts.map +1 -0
- package/dist/client/components/DocumentProvider.js +47 -0
- package/dist/client/components/DocumentView.d.ts +6 -0
- package/dist/client/components/DocumentView.d.ts.map +1 -0
- package/dist/client/components/DocumentView.js +58 -0
- package/dist/client/components/DragOverlayItem.d.ts +5 -0
- package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
- package/dist/client/components/DragOverlayItem.js +9 -0
- package/dist/client/components/EmojiPicker.d.ts +8 -0
- package/dist/client/components/EmojiPicker.d.ts.map +1 -0
- package/dist/client/components/EmojiPicker.js +48 -0
- package/dist/client/components/ExportButton.d.ts +22 -0
- package/dist/client/components/ExportButton.d.ts.map +1 -0
- package/dist/client/components/ExportButton.js +97 -0
- package/dist/client/components/Layout.d.ts +7 -0
- package/dist/client/components/Layout.d.ts.map +1 -0
- package/dist/client/components/Layout.js +172 -0
- package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
- package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
- package/dist/client/components/ReactEmbedDocs.js +8 -0
- package/dist/client/components/SearchInput.d.ts +2 -0
- package/dist/client/components/SearchInput.d.ts.map +1 -0
- package/dist/client/components/SearchInput.js +7 -0
- package/dist/client/components/Sidebar.d.ts +10 -0
- package/dist/client/components/Sidebar.d.ts.map +1 -0
- package/dist/client/components/Sidebar.js +176 -0
- package/dist/client/components/SortableTreeItem.d.ts +13 -0
- package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
- package/dist/client/components/SortableTreeItem.js +24 -0
- package/dist/client/components/VersionHistory.d.ts +14 -0
- package/dist/client/components/VersionHistory.d.ts.map +1 -0
- package/dist/client/components/VersionHistory.js +102 -0
- package/dist/client/hooks/useCollaboration.d.ts +99 -0
- package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/client/hooks/useCollaboration.js +180 -0
- package/dist/client/hooks/useDocsQuery.d.ts +84 -0
- package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
- package/dist/client/hooks/useDocsQuery.js +241 -0
- package/dist/client/hooks/useExport.d.ts +31 -0
- package/dist/client/hooks/useExport.d.ts.map +1 -0
- package/dist/client/hooks/useExport.js +66 -0
- package/dist/client/hooks/useFileUpload.d.ts +44 -0
- package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/client/hooks/useFileUpload.js +193 -0
- package/dist/client/hooks/useSystemTheme.d.ts +2 -0
- package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/client/hooks/useSystemTheme.js +19 -0
- package/dist/client/hooks/useVersions.d.ts +105 -0
- package/dist/client/hooks/useVersions.d.ts.map +1 -0
- package/dist/client/hooks/useVersions.js +129 -0
- package/dist/client/index.d.ts +23 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/lib/blocknoteTheme.d.ts +13 -0
- package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
- package/dist/client/lib/blocknoteTheme.js +76 -0
- package/dist/client/lib/path.d.ts +8 -0
- package/dist/client/lib/path.d.ts.map +1 -0
- package/dist/client/lib/path.js +30 -0
- package/dist/client/providers/DocumentProvider.d.ts +1 -0
- package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
- package/dist/client/providers/DocumentProvider.js +1 -0
- package/dist/server/CollaborationService.d.ts +134 -0
- package/dist/server/CollaborationService.d.ts.map +1 -0
- package/dist/server/CollaborationService.js +307 -0
- package/dist/server/DocsService.d.ts +115 -0
- package/dist/server/DocsService.d.ts.map +1 -0
- package/dist/server/DocsService.js +512 -0
- package/dist/server/ExportService.d.ts +106 -0
- package/dist/server/ExportService.d.ts.map +1 -0
- package/dist/server/ExportService.js +501 -0
- package/dist/server/FilesService.d.ts +44 -0
- package/dist/server/FilesService.d.ts.map +1 -0
- package/dist/server/FilesService.js +78 -0
- package/dist/server/VersioningService.d.ts +112 -0
- package/dist/server/VersioningService.d.ts.map +1 -0
- package/dist/server/VersioningService.js +264 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +22 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +483 -0
- package/dist/server/schema.d.ts +587 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +126 -0
- package/dist/shared/types.d.ts +314 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +48 -0
- package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
- package/drizzle/migrations/meta/0000_snapshot.json +574 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/package.json +109 -0
- package/styles/docs.css +981 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nik Lucky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# react-embed-docs
|
|
2
|
+
|
|
3
|
+
Full-stack documentation system with BlockNote editor, hierarchical structure, and file uploads.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📝 **BlockNote Editor** - Notion-style block-based rich text editor
|
|
8
|
+
- 🌳 **Hierarchical Structure** - Parent-child document relationships with drag-drop reordering
|
|
9
|
+
- 🔍 **Full-Text Search** - Fast search with pre-built search index
|
|
10
|
+
- 🎨 **Customizable** - Tailwind CSS styling with dark mode support
|
|
11
|
+
- 📎 **File Uploads** - Built-in cover image support with PostgreSQL storage
|
|
12
|
+
- 😀 **Emoji Icons** - Document icons with emoji picker
|
|
13
|
+
- ⚡ **TypeScript** - Full type safety
|
|
14
|
+
- 🗄️ **PostgreSQL** - Reliable data storage with Drizzle ORM
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Using bun
|
|
20
|
+
bun install react-embed-docs
|
|
21
|
+
|
|
22
|
+
# Using npm
|
|
23
|
+
npm install react-embed-docs
|
|
24
|
+
|
|
25
|
+
# Using yarn
|
|
26
|
+
yarn add react-embed-docs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Peer Dependencies
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"react": "^18.0.0",
|
|
34
|
+
"react-dom": "^18.0.0",
|
|
35
|
+
"@blocknote/core": "^0.46.0",
|
|
36
|
+
"@blocknote/react": "^0.46.0",
|
|
37
|
+
"@blocknote/mantine": "^0.46.0",
|
|
38
|
+
"drizzle-orm": "^0.31.0",
|
|
39
|
+
"@tanstack/react-query": "^5.0.0",
|
|
40
|
+
"postgres": "^3.4.0"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### 1. Database Setup
|
|
47
|
+
|
|
48
|
+
Run the migrations to create the necessary tables:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
52
|
+
import postgres from 'postgres'
|
|
53
|
+
import { runMigrations } from 'react-embed-docs/server'
|
|
54
|
+
|
|
55
|
+
const connection = postgres(process.env.DATABASE_URL)
|
|
56
|
+
const db = drizzle(connection)
|
|
57
|
+
|
|
58
|
+
// Run migrations on startup
|
|
59
|
+
await runMigrations(db)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Server Setup (Hono)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { Hono } from 'hono'
|
|
66
|
+
import { createDocsRouter, createFilesRouter } from 'react-embed-docs/server'
|
|
67
|
+
|
|
68
|
+
const app = new Hono()
|
|
69
|
+
|
|
70
|
+
// Add docs routes
|
|
71
|
+
app.route('/api/docs', createDocsRouter({ db }))
|
|
72
|
+
app.route('/api/files', createFilesRouter({ db }))
|
|
73
|
+
|
|
74
|
+
// Or combine both
|
|
75
|
+
import { createRouter } from 'react-embed-docs/server'
|
|
76
|
+
app.route('/api', createRouter({ db }))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Client Setup (React)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
83
|
+
import { DocsLayout, DocumentEdit } from 'react-embed-docs/client'
|
|
84
|
+
import 'react-embed-docs/styles'
|
|
85
|
+
|
|
86
|
+
const queryClient = new QueryClient()
|
|
87
|
+
|
|
88
|
+
function App() {
|
|
89
|
+
return (
|
|
90
|
+
<QueryClientProvider client={queryClient}>
|
|
91
|
+
<DocsLayout
|
|
92
|
+
onNavigate={(id) => window.location.href = `/docs/${id}`}
|
|
93
|
+
userAvatar={<UserAvatar />} // Your custom avatar component
|
|
94
|
+
>
|
|
95
|
+
<DocumentEdit
|
|
96
|
+
docId="new"
|
|
97
|
+
onSave={(doc) => console.log('Saved:', doc)}
|
|
98
|
+
onNavigate={(id) => window.location.href = `/docs/${id}`}
|
|
99
|
+
/>
|
|
100
|
+
</DocsLayout>
|
|
101
|
+
</QueryClientProvider>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API Reference
|
|
107
|
+
|
|
108
|
+
### Server Exports
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import {
|
|
112
|
+
DocsService,
|
|
113
|
+
FilesService,
|
|
114
|
+
createDocsRouter,
|
|
115
|
+
createFilesRouter,
|
|
116
|
+
createRouter,
|
|
117
|
+
runMigrations,
|
|
118
|
+
documentsTable,
|
|
119
|
+
filesTable
|
|
120
|
+
} from 'react-embed-docs/server'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### DocsService
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
class DocsService {
|
|
127
|
+
// Queries
|
|
128
|
+
list(filters, options): Promise<ListDocumentsResult>
|
|
129
|
+
getById(id: string): Promise<Document | undefined>
|
|
130
|
+
getBySlug(slug: string): Promise<Document | undefined>
|
|
131
|
+
getChildren(parentId: string): Promise<Document[]>
|
|
132
|
+
getTree(): Promise<Document[]>
|
|
133
|
+
|
|
134
|
+
// Mutations
|
|
135
|
+
create(data: InsertDocument): Promise<Document>
|
|
136
|
+
update(id: string, data: UpdateDocument): Promise<Document | undefined>
|
|
137
|
+
delete(id: string): Promise<Document | undefined>
|
|
138
|
+
|
|
139
|
+
// Search
|
|
140
|
+
search(query: string, options): Promise<SearchResult[]>
|
|
141
|
+
searchText(query: string, options): Promise<SearchTextResult[]>
|
|
142
|
+
|
|
143
|
+
// Reordering
|
|
144
|
+
reorder(id: string, newParentId: string | null, newOrder: number): Promise<Document | undefined>
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Client Exports
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import {
|
|
152
|
+
DocsLayout,
|
|
153
|
+
DocumentEdit,
|
|
154
|
+
DocumentView,
|
|
155
|
+
DocumentList,
|
|
156
|
+
EmojiPicker,
|
|
157
|
+
useDocumentsQuery,
|
|
158
|
+
useDocumentQuery,
|
|
159
|
+
useCreateDocumentMutation,
|
|
160
|
+
useUpdateDocumentMutation,
|
|
161
|
+
useDeleteDocumentMutation,
|
|
162
|
+
useReorderDocumentMutation,
|
|
163
|
+
useFileUpload
|
|
164
|
+
} from 'react-embed-docs/client'
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Components
|
|
168
|
+
|
|
169
|
+
**DocsLayout**
|
|
170
|
+
```tsx
|
|
171
|
+
<DocsLayout
|
|
172
|
+
currentDocId?: string // Currently active document ID
|
|
173
|
+
onNavigate?: (id: string) => void // Navigation callback
|
|
174
|
+
userAvatar?: React.ReactNode // User avatar component for header
|
|
175
|
+
onSearch?: (query: string) => void // Search callback
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</DocsLayout>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**DocumentEdit**
|
|
182
|
+
```tsx
|
|
183
|
+
<DocumentEdit
|
|
184
|
+
docId: string // 'new' or document ID
|
|
185
|
+
parentId?: string // Parent document ID (for creating child)
|
|
186
|
+
onSave?: (doc: Document) => void
|
|
187
|
+
onNavigate?: (id: string) => void
|
|
188
|
+
/>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**DocumentView**
|
|
192
|
+
```tsx
|
|
193
|
+
<DocumentView
|
|
194
|
+
docId: string
|
|
195
|
+
onEdit?: (id: string) => void
|
|
196
|
+
onCreateChild?: (parentId: string) => void
|
|
197
|
+
onNavigate?: (id: string) => void
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Hooks
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Queries
|
|
205
|
+
const { data, isLoading } = useDocumentsQuery({ search?: string, parentId?: string })
|
|
206
|
+
const { data } = useDocumentQuery(docId)
|
|
207
|
+
|
|
208
|
+
// Mutations
|
|
209
|
+
const create = useCreateDocumentMutation()
|
|
210
|
+
const update = useUpdateDocumentMutation()
|
|
211
|
+
const delete = useDeleteDocumentMutation()
|
|
212
|
+
const reorder = useReorderDocumentMutation()
|
|
213
|
+
|
|
214
|
+
// File upload
|
|
215
|
+
const { isUploading, progress, error, uploadFile } = useFileUpload({
|
|
216
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
217
|
+
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Database Schema
|
|
222
|
+
|
|
223
|
+
### Documents Table
|
|
224
|
+
|
|
225
|
+
| Column | Type | Description |
|
|
226
|
+
|--------|------|-------------|
|
|
227
|
+
| id | varchar(21) | Primary key (nanoid) |
|
|
228
|
+
| title | varchar(255) | Document title |
|
|
229
|
+
| slug | varchar(255) | URL-friendly unique slug |
|
|
230
|
+
| content | jsonb | BlockNote JSON content |
|
|
231
|
+
| searchIndex | text | Pre-built search text (title + content) |
|
|
232
|
+
| emoji | varchar(10) | Document icon emoji |
|
|
233
|
+
| cover | varchar(500) | Cover image file ID |
|
|
234
|
+
| isPublished | boolean | Published state |
|
|
235
|
+
| parentId | varchar(21) | Parent document ID (self-reference) |
|
|
236
|
+
| order | integer | Sort order within parent |
|
|
237
|
+
| authorId | integer | Document author (optional) |
|
|
238
|
+
| createdAt | timestamp | Creation time |
|
|
239
|
+
| updatedAt | timestamp | Last update time |
|
|
240
|
+
| deletedAt | timestamp | Soft delete timestamp |
|
|
241
|
+
|
|
242
|
+
### Files Table
|
|
243
|
+
|
|
244
|
+
| Column | Type | Description |
|
|
245
|
+
|--------|------|-------------|
|
|
246
|
+
| id | varchar(21) | Primary key (nanoid) |
|
|
247
|
+
| filename | varchar(255) | Original filename |
|
|
248
|
+
| mimeType | varchar(100) | File MIME type |
|
|
249
|
+
| size | integer | File size in bytes |
|
|
250
|
+
| content | text | Base64 encoded file content |
|
|
251
|
+
| createdAt | timestamp | Upload time |
|
|
252
|
+
|
|
253
|
+
## Styling
|
|
254
|
+
|
|
255
|
+
The package includes a complete Tailwind CSS stylesheet with all utilities. Import it in your app:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import 'react-embed-docs/styles'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
This imports a pre-built CSS bundle that includes all Tailwind utilities used by the components. **No additional Tailwind configuration needed.**
|
|
262
|
+
|
|
263
|
+
### Using with your own Tailwind setup
|
|
264
|
+
|
|
265
|
+
If you prefer to process the styles through your own Tailwind build (e.g., for custom theming), use the source CSS instead:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import 'react-embed-docs/styles/source'
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Then add the package files to your Tailwind content config:
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
// tailwind.config.js
|
|
275
|
+
module.exports = {
|
|
276
|
+
content: [
|
|
277
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
278
|
+
'./node_modules/react-embed-docs/dist/client/**/*.js',
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Tailwind CSS Safelist (legacy)
|
|
284
|
+
|
|
285
|
+
If you're using the source CSS and some classes aren't working, you can use the safelist:
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
// tailwind.config.js
|
|
289
|
+
module.exports = {
|
|
290
|
+
content: [
|
|
291
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
292
|
+
// Add this to scan the safelist
|
|
293
|
+
'./node_modules/react-embed-docs/styles/tailwind.safelist.js',
|
|
294
|
+
],
|
|
295
|
+
safelist: [
|
|
296
|
+
// Option 1: Include all classes from the package
|
|
297
|
+
...require('react-embed-docs/styles/tailwind.safelist.js'),
|
|
298
|
+
|
|
299
|
+
// Option 2: Or manually add specific classes you need
|
|
300
|
+
'h-40', 'h-44', 'h-48', 'h-52', 'h-56', 'h-60',
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Dark Mode
|
|
306
|
+
|
|
307
|
+
All components support dark mode through Tailwind's `dark:` variants. The components automatically adapt when you have `class="dark"` on your HTML element.
|
|
308
|
+
|
|
309
|
+
## Configuration
|
|
310
|
+
|
|
311
|
+
### Custom API Prefix
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
app.route('/custom-prefix', createDocsRouter({ db }))
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### File Upload Limits
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const { uploadFile } = useFileUpload({
|
|
321
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
322
|
+
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Authentication
|
|
327
|
+
|
|
328
|
+
The package doesn't enforce authentication, allowing you to integrate with your own auth system:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// Add your auth middleware before docs routes
|
|
332
|
+
app.use('/api/docs/*', yourAuthMiddleware)
|
|
333
|
+
app.route('/api/docs', createDocsRouter({ db }))
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Examples
|
|
337
|
+
|
|
338
|
+
### Creating a New Document
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const createMutation = useCreateDocumentMutation()
|
|
342
|
+
|
|
343
|
+
const handleCreate = async () => {
|
|
344
|
+
const doc = await createMutation.mutateAsync({
|
|
345
|
+
title: 'My New Document',
|
|
346
|
+
slug: 'my-new-document',
|
|
347
|
+
content: [], // BlockNote blocks
|
|
348
|
+
emoji: '📝',
|
|
349
|
+
isPublished: true
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// Navigate to the new document
|
|
353
|
+
router.push(`/docs/${doc.id}`)
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Uploading a Cover Image
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const { uploadFile } = useFileUpload()
|
|
361
|
+
const updateMutation = useUpdateDocumentMutation()
|
|
362
|
+
|
|
363
|
+
const handleCoverUpload = async (file: File) => {
|
|
364
|
+
const result = await uploadFile(file)
|
|
365
|
+
if (result) {
|
|
366
|
+
await updateMutation.mutateAsync({
|
|
367
|
+
id: docId,
|
|
368
|
+
cover: result.url // /api/files/{id}
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Searching Documents
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
378
|
+
const { data } = useDocumentsQuery({ search: searchQuery })
|
|
379
|
+
|
|
380
|
+
// Results include documents matching title or content
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Migration Guide
|
|
384
|
+
|
|
385
|
+
### From Existing Implementation
|
|
386
|
+
|
|
387
|
+
If you're migrating from an existing docs implementation:
|
|
388
|
+
|
|
389
|
+
1. **Database**: The schema is compatible but adds `searchIndex` column for faster search
|
|
390
|
+
2. **Components**: Replace shadcn imports with `react-embed-docs/client`
|
|
391
|
+
3. **Services**: Replace local services with `react-embed-docs/server`
|
|
392
|
+
4. **Styling**: Import `react-embed-docs/styles` or copy CSS classes
|
|
393
|
+
|
|
394
|
+
## Development
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# Install dependencies
|
|
398
|
+
bun install
|
|
399
|
+
|
|
400
|
+
# Build
|
|
401
|
+
bun run build
|
|
402
|
+
|
|
403
|
+
# Watch mode
|
|
404
|
+
bun run dev
|
|
405
|
+
|
|
406
|
+
# Run migrations
|
|
407
|
+
bun run migrate
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## License
|
|
411
|
+
|
|
412
|
+
MIT License - see LICENSE file for details.
|
|
413
|
+
|
|
414
|
+
## Contributing
|
|
415
|
+
|
|
416
|
+
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
|
|
417
|
+
|
|
418
|
+
## Support
|
|
419
|
+
|
|
420
|
+
- 📧 Email: support@example.com
|
|
421
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/niklucky/react-embed-docs/issues)
|
|
422
|
+
- 📖 Documentation: [Full Documentation](https://github.com/niklucky/react-embed-docs#readme)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface BreadcrumbDoc {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
emoji?: string;
|
|
6
|
+
parentId?: string | null;
|
|
7
|
+
}
|
|
8
|
+
interface BreadcrumbsProps {
|
|
9
|
+
docId?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Array of all documents to build paths locally without API call.
|
|
12
|
+
* If not provided, will fetch from API.
|
|
13
|
+
*/
|
|
14
|
+
documents?: BreadcrumbDoc[];
|
|
15
|
+
onNavigate?: (path: string) => void;
|
|
16
|
+
homeLabel?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function Breadcrumbs({ docId, documents, onNavigate, homeLabel, className, }: BreadcrumbsProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=Breadcrumbs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Breadcrumbs.d.ts","sourceRoot":"","sources":["../../../src/client/components/Breadcrumbs.tsx"],"names":[],"mappings":"AAKA,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,aAAa,EAAE,CAAA;IAC3B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwCD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,SAAS,EACT,UAAU,EACV,SAAkB,EAClB,SAAc,GACf,EAAE,gBAAgB,2CAmLlB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronRight, Home, Loader2 } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
// Fetch breadcrumbs from API
|
|
6
|
+
async function fetchBreadcrumbs(docId) {
|
|
7
|
+
const response = await fetch(`/api/docs/${docId}/breadcrumbs`);
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
throw new Error('Failed to fetch breadcrumbs');
|
|
10
|
+
}
|
|
11
|
+
const data = await response.json();
|
|
12
|
+
return data.items;
|
|
13
|
+
}
|
|
14
|
+
// Build full path from root to document
|
|
15
|
+
function buildPath(docId, allDocs, basePath = '/docs') {
|
|
16
|
+
const pathParts = [];
|
|
17
|
+
const visited = new Set();
|
|
18
|
+
let currentId = docId;
|
|
19
|
+
while (currentId) {
|
|
20
|
+
// Prevent infinite loops from circular references
|
|
21
|
+
if (visited.has(currentId))
|
|
22
|
+
break;
|
|
23
|
+
visited.add(currentId);
|
|
24
|
+
const doc = allDocs.find((d) => d.id === currentId);
|
|
25
|
+
if (!doc)
|
|
26
|
+
break;
|
|
27
|
+
pathParts.unshift(doc.slug || doc.id);
|
|
28
|
+
currentId = allDocs.find((d) => d.id === currentId)?.parentId ?? null;
|
|
29
|
+
}
|
|
30
|
+
if (pathParts.length === 0)
|
|
31
|
+
return basePath;
|
|
32
|
+
return `${basePath}/${pathParts.join('/')}`;
|
|
33
|
+
}
|
|
34
|
+
export function Breadcrumbs({ docId, documents, onNavigate, homeLabel = 'Home', className = '', }) {
|
|
35
|
+
const [fetchedBreadcrumbs, setFetchedBreadcrumbs] = useState([]);
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
// Use provided documents or fetch from API
|
|
39
|
+
const breadcrumbs = useMemo(() => {
|
|
40
|
+
if (documents && docId) {
|
|
41
|
+
// Build breadcrumbs from provided documents
|
|
42
|
+
const result = [];
|
|
43
|
+
const visited = new Set();
|
|
44
|
+
let currentId = docId;
|
|
45
|
+
while (currentId) {
|
|
46
|
+
if (visited.has(currentId))
|
|
47
|
+
break;
|
|
48
|
+
visited.add(currentId);
|
|
49
|
+
const doc = documents.find((d) => d.id === currentId);
|
|
50
|
+
if (!doc)
|
|
51
|
+
break;
|
|
52
|
+
result.unshift(doc);
|
|
53
|
+
currentId = documents.find((d) => d.id === currentId)?.parentId ?? null;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
return fetchedBreadcrumbs;
|
|
58
|
+
}, [documents, docId, fetchedBreadcrumbs]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
// If documents are provided, don't fetch
|
|
61
|
+
if (documents || !docId) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setIsLoading(true);
|
|
65
|
+
setError(null);
|
|
66
|
+
fetchBreadcrumbs(docId)
|
|
67
|
+
.then((items) => {
|
|
68
|
+
setFetchedBreadcrumbs(items);
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
})
|
|
71
|
+
.catch((err) => {
|
|
72
|
+
setError(err instanceof Error ? err.message : 'Failed to load breadcrumbs');
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
});
|
|
75
|
+
}, [docId, documents]);
|
|
76
|
+
const handleNavigate = (targetDocId) => {
|
|
77
|
+
if (!onNavigate)
|
|
78
|
+
return;
|
|
79
|
+
if (!targetDocId) {
|
|
80
|
+
// Navigate to root
|
|
81
|
+
onNavigate('/docs');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Build full path
|
|
85
|
+
const pathParts = [];
|
|
86
|
+
const visited = new Set();
|
|
87
|
+
let currentId = targetDocId;
|
|
88
|
+
const allDocs = documents || breadcrumbs;
|
|
89
|
+
while (currentId) {
|
|
90
|
+
if (visited.has(currentId))
|
|
91
|
+
break;
|
|
92
|
+
visited.add(currentId);
|
|
93
|
+
const doc = allDocs.find((d) => d.id === currentId);
|
|
94
|
+
if (!doc)
|
|
95
|
+
break;
|
|
96
|
+
pathParts.unshift(doc.slug || doc.id);
|
|
97
|
+
currentId = allDocs.find((d) => d.id === currentId)?.parentId ?? null;
|
|
98
|
+
}
|
|
99
|
+
if (pathParts.length === 0) {
|
|
100
|
+
onNavigate('/docs');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
onNavigate(`/docs/${pathParts.join('/')}`);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (!docId) {
|
|
107
|
+
return (_jsx("nav", { className: `flex items-center gap-1 text-sm ${className}`, children: _jsxs("button", { onClick: () => handleNavigate(null), className: "flex items-center gap-1.5 text-gray-500 hover:text-gray-700 transition-colors", children: [_jsx(Home, { className: "h-4 w-4" }), _jsx("span", { children: homeLabel })] }) }));
|
|
108
|
+
}
|
|
109
|
+
if (isLoading && !documents) {
|
|
110
|
+
return (_jsxs("div", { className: `flex items-center gap-2 text-sm text-gray-400 ${className}`, children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), _jsx("span", { children: "Loading..." })] }));
|
|
111
|
+
}
|
|
112
|
+
if (error && !documents) {
|
|
113
|
+
return (_jsxs("div", { className: `flex items-center gap-1 text-sm text-gray-500 ${className}`, children: [_jsxs("button", { onClick: () => handleNavigate(null), className: "flex items-center gap-1.5 hover:text-gray-700 transition-colors", children: [_jsx(Home, { className: "h-4 w-4" }), _jsx("span", { children: homeLabel })] }), _jsx(ChevronRight, { className: "h-4 w-4 text-gray-400" }), _jsx("span", { className: "text-gray-400", children: "Error loading path" })] }));
|
|
114
|
+
}
|
|
115
|
+
if (breadcrumbs.length === 0) {
|
|
116
|
+
return (_jsx("nav", { className: `flex items-center gap-1 text-sm ${className}`, children: _jsxs("button", { onClick: () => handleNavigate(null), className: "flex items-center gap-1.5 text-gray-500 hover:text-gray-700 transition-colors", children: [_jsx(Home, { className: "h-4 w-4" }), _jsx("span", { children: homeLabel })] }) }));
|
|
117
|
+
}
|
|
118
|
+
return (_jsxs("nav", { className: `flex items-center gap-1 text-sm ${className}`, children: [_jsxs("button", { onClick: () => handleNavigate(null), className: "flex items-center gap-1.5 text-gray-500 hover:text-gray-700 transition-colors shrink-0", title: homeLabel, children: [_jsx(Home, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: homeLabel })] }), breadcrumbs.map((doc, index) => {
|
|
119
|
+
const isLast = index === breadcrumbs.length - 1;
|
|
120
|
+
const displayTitle = doc.title || 'Untitled';
|
|
121
|
+
return (_jsxs("span", { className: "flex items-center gap-1 min-w-0", children: [_jsx(ChevronRight, { className: "h-4 w-4 text-gray-400 shrink-0" }), isLast ? (_jsxs("span", { className: "font-medium text-gray-900 truncate max-w-[200px] sm:max-w-[300px] md:max-w-[400px]", title: displayTitle, children: [doc.emoji && _jsx("span", { className: "mr-1", children: doc.emoji }), displayTitle] })) : (_jsxs("button", { onClick: () => handleNavigate(doc.id), className: "text-gray-500 hover:text-gray-700 transition-colors truncate max-w-[150px] sm:max-w-[200px]", title: displayTitle, children: [doc.emoji && _jsx("span", { className: "mr-1", children: doc.emoji }), displayTitle] }))] }, doc.id));
|
|
122
|
+
})] }));
|
|
123
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DocumentSummary } from '../../shared/types.js';
|
|
2
|
+
interface DocsLayoutProps {
|
|
3
|
+
theme?: 'light' | 'dark';
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
currentDocId?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Called when navigating to a document.
|
|
8
|
+
* `path` is the full path from root to the document (e.g., "/docs/parent/child")
|
|
9
|
+
*/
|
|
10
|
+
onNavigate?: (path: string) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Base path for document URLs (default: "/docs")
|
|
13
|
+
*/
|
|
14
|
+
basePath?: string;
|
|
15
|
+
userAvatar?: React.ReactNode;
|
|
16
|
+
onSearch?: (query: string) => DocumentSummary[];
|
|
17
|
+
}
|
|
18
|
+
export declare function DocsLayout({ children, currentDocId, onNavigate, basePath, userAvatar, onSearch, }: DocsLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=DocsLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocsLayout.d.ts","sourceRoot":"","sources":["../../../src/client/components/DocsLayout.tsx"],"names":[],"mappings":"AAoCA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAavD,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,eAAe,EAAE,CAAA;CAChD;AAsQD,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,QAAkB,EAClB,UAAU,EACV,QAAQ,GACT,EAAE,eAAe,2CAgcjB"}
|