prev-cli 0.7.2 → 0.8.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/dist/cli.js +2 -2
- package/package.json +1 -1
- package/src/theme/Layout.tsx +317 -46
- package/src/theme/MetadataBlock.tsx +17 -28
- package/src/theme/entry.tsx +1 -5
- package/src/theme/styles.css +54 -19
package/dist/cli.js
CHANGED
|
@@ -139,8 +139,8 @@ function fileToRoute(file) {
|
|
|
139
139
|
async function scanPages(rootDir) {
|
|
140
140
|
const files = await fg.glob("**/*.{md,mdx}", {
|
|
141
141
|
cwd: rootDir,
|
|
142
|
-
ignore: ["node_modules/**", "dist/**", ".cache/**"],
|
|
143
|
-
dot:
|
|
142
|
+
ignore: ["node_modules/**", "dist/**", ".cache/**", ".*/**"],
|
|
143
|
+
dot: false
|
|
144
144
|
});
|
|
145
145
|
const routeMap = new Map;
|
|
146
146
|
for (const file of files) {
|
package/package.json
CHANGED
package/src/theme/Layout.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
1
|
+
import React, { useState, useEffect, useRef, createContext, useContext } from 'react'
|
|
2
2
|
import { Link, useLocation } from '@tanstack/react-router'
|
|
3
3
|
import type { PageTree } from 'fumadocs-core/server'
|
|
4
4
|
|
|
@@ -7,6 +7,77 @@ interface LayoutProps {
|
|
|
7
7
|
children: React.ReactNode
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
type TreeItem = PageTree.Item | PageTree.Folder
|
|
11
|
+
|
|
12
|
+
// Get unique ID for a tree item
|
|
13
|
+
function getItemId(item: TreeItem): string {
|
|
14
|
+
return item.type === 'folder' ? `folder:${item.name}` : item.url
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Storage key for a specific path (root or folder)
|
|
18
|
+
function getStorageKey(path: string): string {
|
|
19
|
+
return `sidebar-order:${path}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Apply saved order to items for a specific path
|
|
23
|
+
// Handles: new items (appended), removed items (skipped), stale storage (cleaned)
|
|
24
|
+
function applyOrder(items: TreeItem[], path: string): TreeItem[] {
|
|
25
|
+
if (typeof window === 'undefined') return items
|
|
26
|
+
|
|
27
|
+
const saved = localStorage.getItem(getStorageKey(path))
|
|
28
|
+
if (!saved) return items
|
|
29
|
+
|
|
30
|
+
const order: string[] = JSON.parse(saved)
|
|
31
|
+
if (order.length === 0) return items
|
|
32
|
+
|
|
33
|
+
const itemMap = new Map(items.map(item => [getItemId(item), item]))
|
|
34
|
+
const ordered: TreeItem[] = []
|
|
35
|
+
|
|
36
|
+
// Add items in saved order (skip removed ones)
|
|
37
|
+
for (const id of order) {
|
|
38
|
+
const item = itemMap.get(id)
|
|
39
|
+
if (item) {
|
|
40
|
+
ordered.push(item)
|
|
41
|
+
itemMap.delete(id)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add remaining items (new ones not in saved order)
|
|
46
|
+
for (const item of itemMap.values()) {
|
|
47
|
+
ordered.push(item)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Clean up stale entries: if order changed, update storage
|
|
51
|
+
const currentIds = ordered.map(getItemId)
|
|
52
|
+
const hasChanges = order.length !== currentIds.length ||
|
|
53
|
+
order.some((id, i) => currentIds[i] !== id)
|
|
54
|
+
|
|
55
|
+
if (hasChanges) {
|
|
56
|
+
localStorage.setItem(getStorageKey(path), JSON.stringify(currentIds))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return ordered
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Save order for a specific path
|
|
63
|
+
function saveOrder(items: TreeItem[], path: string): void {
|
|
64
|
+
const order = items.map(getItemId)
|
|
65
|
+
localStorage.setItem(getStorageKey(path), JSON.stringify(order))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Drag context for nested components
|
|
69
|
+
interface DragContextType {
|
|
70
|
+
dragPath: string | null
|
|
71
|
+
dragIndex: number | null
|
|
72
|
+
dragOverPath: string | null
|
|
73
|
+
dragOverIndex: number | null
|
|
74
|
+
onDragStart: (path: string, index: number) => void
|
|
75
|
+
onDragOver: (path: string, index: number) => void
|
|
76
|
+
onDragEnd: () => void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const DragContext = createContext<DragContextType | null>(null)
|
|
80
|
+
|
|
10
81
|
function SidebarToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
|
|
11
82
|
return (
|
|
12
83
|
<button
|
|
@@ -41,23 +112,55 @@ function CollapsedFolderIcon({ item, onClick }: { item: PageTree.Folder; onClick
|
|
|
41
112
|
)
|
|
42
113
|
}
|
|
43
114
|
|
|
44
|
-
interface
|
|
45
|
-
item:
|
|
115
|
+
interface DraggableSidebarItemProps {
|
|
116
|
+
item: TreeItem
|
|
117
|
+
index: number
|
|
118
|
+
path: string
|
|
46
119
|
depth?: number
|
|
47
120
|
}
|
|
48
121
|
|
|
49
|
-
function
|
|
122
|
+
function DraggableSidebarItem({ item, index, path, depth = 0 }: DraggableSidebarItemProps) {
|
|
50
123
|
const location = useLocation()
|
|
51
124
|
const [isOpen, setIsOpen] = useState(true)
|
|
125
|
+
const dragCtx = useContext(DragContext)
|
|
126
|
+
|
|
127
|
+
const handleDragStart = (e: React.DragEvent) => {
|
|
128
|
+
e.stopPropagation()
|
|
129
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
130
|
+
dragCtx?.onDragStart(path, index)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
134
|
+
e.preventDefault()
|
|
135
|
+
e.stopPropagation()
|
|
136
|
+
e.dataTransfer.dropEffect = 'move'
|
|
137
|
+
dragCtx?.onDragOver(path, index)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const handleDragEnd = (e: React.DragEvent) => {
|
|
141
|
+
e.stopPropagation()
|
|
142
|
+
dragCtx?.onDragEnd()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isDropTarget = dragCtx?.dragOverPath === path && dragCtx?.dragOverIndex === index
|
|
52
146
|
|
|
53
147
|
if (item.type === 'folder') {
|
|
148
|
+
const folderPath = `${path}/${item.name}`
|
|
149
|
+
|
|
54
150
|
return (
|
|
55
|
-
<div
|
|
151
|
+
<div
|
|
152
|
+
className={`sidebar-folder ${isDropTarget ? 'drop-target' : ''}`}
|
|
153
|
+
draggable
|
|
154
|
+
onDragStart={handleDragStart}
|
|
155
|
+
onDragOver={handleDragOver}
|
|
156
|
+
onDragEnd={handleDragEnd}
|
|
157
|
+
>
|
|
56
158
|
<button
|
|
57
159
|
className="sidebar-folder-toggle"
|
|
58
160
|
onClick={() => setIsOpen(!isOpen)}
|
|
59
161
|
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
60
162
|
>
|
|
163
|
+
<span className="drag-handle">⠿</span>
|
|
61
164
|
<span className={`folder-icon ${isOpen ? 'open' : ''}`}>
|
|
62
165
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
63
166
|
<path d="M4.5 2L8.5 6L4.5 10" stroke="currentColor" strokeWidth="1.5" fill="none"/>
|
|
@@ -66,11 +169,11 @@ function SidebarItem({ item, depth = 0 }: SidebarItemProps) {
|
|
|
66
169
|
{item.name}
|
|
67
170
|
</button>
|
|
68
171
|
{isOpen && (
|
|
69
|
-
<
|
|
70
|
-
{item.children
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
172
|
+
<DraggableFolderChildren
|
|
173
|
+
items={item.children}
|
|
174
|
+
path={folderPath}
|
|
175
|
+
depth={depth + 1}
|
|
176
|
+
/>
|
|
74
177
|
)}
|
|
75
178
|
</div>
|
|
76
179
|
)
|
|
@@ -80,13 +183,81 @@ function SidebarItem({ item, depth = 0 }: SidebarItemProps) {
|
|
|
80
183
|
(item.url === '/' && location.pathname === '/')
|
|
81
184
|
|
|
82
185
|
return (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
186
|
+
<div
|
|
187
|
+
className={`sidebar-item-wrapper ${isDropTarget ? 'drop-target' : ''}`}
|
|
188
|
+
draggable
|
|
189
|
+
onDragStart={handleDragStart}
|
|
190
|
+
onDragOver={handleDragOver}
|
|
191
|
+
onDragEnd={handleDragEnd}
|
|
87
192
|
>
|
|
88
|
-
|
|
89
|
-
|
|
193
|
+
<Link
|
|
194
|
+
to={item.url}
|
|
195
|
+
className={`sidebar-link ${isActive ? 'active' : ''}`}
|
|
196
|
+
style={{ paddingLeft: `${depth * 12 + 16}px` }}
|
|
197
|
+
>
|
|
198
|
+
<span className="drag-handle">⠿</span>
|
|
199
|
+
{item.name}
|
|
200
|
+
</Link>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Draggable folder children with their own order state
|
|
206
|
+
function DraggableFolderChildren({ items, path, depth }: { items: TreeItem[]; path: string; depth: number }) {
|
|
207
|
+
const [orderedItems, setOrderedItems] = useState<TreeItem[]>(() => applyOrder(items, path))
|
|
208
|
+
const dragCtx = useContext(DragContext)
|
|
209
|
+
|
|
210
|
+
// Update when items change
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
setOrderedItems(applyOrder(items, path))
|
|
213
|
+
}, [items, path])
|
|
214
|
+
|
|
215
|
+
// Handle reorder when drag ends in this folder
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (
|
|
218
|
+
dragCtx?.dragPath === path &&
|
|
219
|
+
dragCtx?.dragOverPath === path &&
|
|
220
|
+
dragCtx?.dragIndex !== null &&
|
|
221
|
+
dragCtx?.dragOverIndex !== null &&
|
|
222
|
+
dragCtx?.dragIndex !== dragCtx?.dragOverIndex
|
|
223
|
+
) {
|
|
224
|
+
// Will be handled by parent's onDragEnd
|
|
225
|
+
}
|
|
226
|
+
}, [dragCtx, path])
|
|
227
|
+
|
|
228
|
+
// Listen for successful drops in this path
|
|
229
|
+
const prevDragCtx = useRef(dragCtx)
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const prev = prevDragCtx.current
|
|
232
|
+
if (
|
|
233
|
+
prev?.dragPath === path &&
|
|
234
|
+
prev?.dragOverPath === path &&
|
|
235
|
+
prev?.dragIndex !== null &&
|
|
236
|
+
prev?.dragOverIndex !== null &&
|
|
237
|
+
prev?.dragIndex !== prev?.dragOverIndex &&
|
|
238
|
+
dragCtx?.dragPath === null // drag ended
|
|
239
|
+
) {
|
|
240
|
+
const newItems = [...orderedItems]
|
|
241
|
+
const [removed] = newItems.splice(prev.dragIndex, 1)
|
|
242
|
+
newItems.splice(prev.dragOverIndex, 0, removed)
|
|
243
|
+
setOrderedItems(newItems)
|
|
244
|
+
saveOrder(newItems, path)
|
|
245
|
+
}
|
|
246
|
+
prevDragCtx.current = dragCtx
|
|
247
|
+
}, [dragCtx, path, orderedItems])
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className="sidebar-folder-children">
|
|
251
|
+
{orderedItems.map((child, i) => (
|
|
252
|
+
<DraggableSidebarItem
|
|
253
|
+
key={getItemId(child)}
|
|
254
|
+
item={child}
|
|
255
|
+
index={i}
|
|
256
|
+
path={path}
|
|
257
|
+
depth={depth}
|
|
258
|
+
/>
|
|
259
|
+
))}
|
|
260
|
+
</div>
|
|
90
261
|
)
|
|
91
262
|
}
|
|
92
263
|
|
|
@@ -121,6 +292,41 @@ function ThemeToggle() {
|
|
|
121
292
|
)
|
|
122
293
|
}
|
|
123
294
|
|
|
295
|
+
function WidthToggle() {
|
|
296
|
+
const [isFullWidth, setIsFullWidth] = useState(() => {
|
|
297
|
+
if (typeof window !== 'undefined') {
|
|
298
|
+
return localStorage.getItem('content-full-width') === 'true'
|
|
299
|
+
}
|
|
300
|
+
return false
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
document.documentElement.classList.toggle('full-width', isFullWidth)
|
|
305
|
+
}, [isFullWidth])
|
|
306
|
+
|
|
307
|
+
const toggle = () => {
|
|
308
|
+
const newFullWidth = !isFullWidth
|
|
309
|
+
setIsFullWidth(newFullWidth)
|
|
310
|
+
localStorage.setItem('content-full-width', String(newFullWidth))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<button className="theme-toggle" onClick={toggle} aria-label="Toggle content width">
|
|
315
|
+
{isFullWidth ? (
|
|
316
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
317
|
+
<path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/>
|
|
318
|
+
</svg>
|
|
319
|
+
) : (
|
|
320
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
321
|
+
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
|
|
322
|
+
</svg>
|
|
323
|
+
)}
|
|
324
|
+
</button>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const ROOT_PATH = 'root'
|
|
329
|
+
|
|
124
330
|
export function Layout({ tree, children }: LayoutProps) {
|
|
125
331
|
const [collapsed, setCollapsed] = useState(() => {
|
|
126
332
|
if (typeof window !== 'undefined') {
|
|
@@ -129,6 +335,13 @@ export function Layout({ tree, children }: LayoutProps) {
|
|
|
129
335
|
return false
|
|
130
336
|
})
|
|
131
337
|
|
|
338
|
+
const [orderedItems, setOrderedItems] = useState<TreeItem[]>(() => applyOrder(tree.children, ROOT_PATH))
|
|
339
|
+
|
|
340
|
+
const [dragPath, setDragPath] = useState<string | null>(null)
|
|
341
|
+
const [dragIndex, setDragIndex] = useState<number | null>(null)
|
|
342
|
+
const [dragOverPath, setDragOverPath] = useState<string | null>(null)
|
|
343
|
+
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
|
|
344
|
+
|
|
132
345
|
// Initialize theme from localStorage
|
|
133
346
|
useEffect(() => {
|
|
134
347
|
const saved = localStorage.getItem('theme')
|
|
@@ -137,6 +350,11 @@ export function Layout({ tree, children }: LayoutProps) {
|
|
|
137
350
|
document.documentElement.classList.toggle('dark', isDark)
|
|
138
351
|
}, [])
|
|
139
352
|
|
|
353
|
+
// Update ordered items when tree changes
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
setOrderedItems(applyOrder(tree.children, ROOT_PATH))
|
|
356
|
+
}, [tree.children])
|
|
357
|
+
|
|
140
358
|
// Persist collapsed state
|
|
141
359
|
useEffect(() => {
|
|
142
360
|
localStorage.setItem('sidebar-collapsed', String(collapsed))
|
|
@@ -144,43 +362,96 @@ export function Layout({ tree, children }: LayoutProps) {
|
|
|
144
362
|
|
|
145
363
|
const toggleCollapsed = () => setCollapsed(!collapsed)
|
|
146
364
|
|
|
365
|
+
const handleDragStart = (path: string, index: number) => {
|
|
366
|
+
setDragPath(path)
|
|
367
|
+
setDragIndex(index)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const handleDragOver = (path: string, index: number) => {
|
|
371
|
+
// Only allow dropping in the same path (no cross-folder drops)
|
|
372
|
+
if (dragPath === path && dragIndex !== index) {
|
|
373
|
+
setDragOverPath(path)
|
|
374
|
+
setDragOverIndex(index)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const handleDragEnd = () => {
|
|
379
|
+
// Handle root-level reordering
|
|
380
|
+
if (
|
|
381
|
+
dragPath === ROOT_PATH &&
|
|
382
|
+
dragOverPath === ROOT_PATH &&
|
|
383
|
+
dragIndex !== null &&
|
|
384
|
+
dragOverIndex !== null &&
|
|
385
|
+
dragIndex !== dragOverIndex
|
|
386
|
+
) {
|
|
387
|
+
const newItems = [...orderedItems]
|
|
388
|
+
const [removed] = newItems.splice(dragIndex, 1)
|
|
389
|
+
newItems.splice(dragOverIndex, 0, removed)
|
|
390
|
+
setOrderedItems(newItems)
|
|
391
|
+
saveOrder(newItems, ROOT_PATH)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
setDragPath(null)
|
|
395
|
+
setDragIndex(null)
|
|
396
|
+
setDragOverPath(null)
|
|
397
|
+
setDragOverIndex(null)
|
|
398
|
+
}
|
|
399
|
+
|
|
147
400
|
// Get top-level folders for collapsed rail
|
|
148
|
-
const topFolders =
|
|
401
|
+
const topFolders = orderedItems.filter(
|
|
149
402
|
(item): item is PageTree.Folder => item.type === 'folder'
|
|
150
403
|
)
|
|
151
404
|
|
|
405
|
+
const dragContextValue: DragContextType = {
|
|
406
|
+
dragPath,
|
|
407
|
+
dragIndex,
|
|
408
|
+
dragOverPath,
|
|
409
|
+
dragOverIndex,
|
|
410
|
+
onDragStart: handleDragStart,
|
|
411
|
+
onDragOver: handleDragOver,
|
|
412
|
+
onDragEnd: handleDragEnd,
|
|
413
|
+
}
|
|
414
|
+
|
|
152
415
|
return (
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
{collapsed ? (
|
|
159
|
-
<div className="sidebar-rail">
|
|
160
|
-
{topFolders.map((folder, i) => (
|
|
161
|
-
<CollapsedFolderIcon
|
|
162
|
-
key={i}
|
|
163
|
-
item={folder}
|
|
164
|
-
onClick={toggleCollapsed}
|
|
165
|
-
/>
|
|
166
|
-
))}
|
|
416
|
+
<DragContext.Provider value={dragContextValue}>
|
|
417
|
+
<div className={`prev-layout ${collapsed ? 'sidebar-collapsed' : ''}`}>
|
|
418
|
+
<aside className="prev-sidebar">
|
|
419
|
+
<div className="sidebar-header">
|
|
420
|
+
<SidebarToggle collapsed={collapsed} onToggle={toggleCollapsed} />
|
|
167
421
|
</div>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
422
|
+
{collapsed ? (
|
|
423
|
+
<div className="sidebar-rail">
|
|
424
|
+
{topFolders.map((folder, i) => (
|
|
425
|
+
<CollapsedFolderIcon
|
|
426
|
+
key={i}
|
|
427
|
+
item={folder}
|
|
428
|
+
onClick={toggleCollapsed}
|
|
429
|
+
/>
|
|
173
430
|
))}
|
|
174
|
-
</nav>
|
|
175
|
-
<div className="sidebar-footer">
|
|
176
|
-
<ThemeToggle />
|
|
177
431
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
432
|
+
) : (
|
|
433
|
+
<>
|
|
434
|
+
<nav className="sidebar-nav">
|
|
435
|
+
{orderedItems.map((item, i) => (
|
|
436
|
+
<DraggableSidebarItem
|
|
437
|
+
key={getItemId(item)}
|
|
438
|
+
item={item}
|
|
439
|
+
index={i}
|
|
440
|
+
path={ROOT_PATH}
|
|
441
|
+
/>
|
|
442
|
+
))}
|
|
443
|
+
</nav>
|
|
444
|
+
<div className="sidebar-footer">
|
|
445
|
+
<WidthToggle />
|
|
446
|
+
<ThemeToggle />
|
|
447
|
+
</div>
|
|
448
|
+
</>
|
|
449
|
+
)}
|
|
450
|
+
</aside>
|
|
451
|
+
<main className="prev-main">
|
|
452
|
+
{children}
|
|
453
|
+
</main>
|
|
454
|
+
</div>
|
|
455
|
+
</DragContext.Provider>
|
|
185
456
|
)
|
|
186
457
|
}
|
|
@@ -4,8 +4,6 @@ type FrontmatterValue = string | number | boolean | string[] | unknown
|
|
|
4
4
|
|
|
5
5
|
interface MetadataBlockProps {
|
|
6
6
|
frontmatter: Record<string, FrontmatterValue>
|
|
7
|
-
title?: string
|
|
8
|
-
description?: string
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
// Fields to skip (shown elsewhere or internal)
|
|
@@ -102,7 +100,7 @@ function renderValue(key: string, value: FrontmatterValue): React.ReactNode {
|
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
export function MetadataBlock({ frontmatter
|
|
103
|
+
export function MetadataBlock({ frontmatter }: MetadataBlockProps) {
|
|
106
104
|
// Filter out skipped fields and empty values
|
|
107
105
|
const fields = Object.entries(frontmatter).filter(
|
|
108
106
|
([key, value]) => !SKIP_FIELDS.has(key) && value !== undefined && value !== null && value !== ''
|
|
@@ -111,36 +109,27 @@ export function MetadataBlock({ frontmatter, title, description }: MetadataBlock
|
|
|
111
109
|
// Check for draft status
|
|
112
110
|
const isDraft = frontmatter.draft === true
|
|
113
111
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
const hasExplicitDescription = 'description' in frontmatter && frontmatter.description
|
|
117
|
-
|
|
118
|
-
// If no explicit metadata and no other fields, don't render the block
|
|
119
|
-
if (fields.length === 0 && !hasExplicitTitle && !hasExplicitDescription && !isDraft) {
|
|
112
|
+
// If no fields to show, don't render
|
|
113
|
+
if (fields.length === 0 && !isDraft) {
|
|
120
114
|
return null
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
return (
|
|
124
118
|
<div className="metadata-block">
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</span>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{renderValue(key, value)}
|
|
140
|
-
</span>
|
|
141
|
-
))}
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
119
|
+
<div className="metadata-fields">
|
|
120
|
+
{isDraft && (
|
|
121
|
+
<span className="metadata-field metadata-draft">
|
|
122
|
+
<span className="metadata-chip is-draft">Draft</span>
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
{fields.filter(([key]) => key !== 'draft').map(([key, value]) => (
|
|
126
|
+
<span key={key} className="metadata-field">
|
|
127
|
+
{FIELD_ICONS[key] && <span className="metadata-icon">{FIELD_ICONS[key]}</span>}
|
|
128
|
+
<span className="metadata-key">{key}:</span>
|
|
129
|
+
{renderValue(key, value)}
|
|
130
|
+
</span>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
144
133
|
</div>
|
|
145
134
|
)
|
|
146
135
|
}
|
package/src/theme/entry.tsx
CHANGED
|
@@ -74,11 +74,7 @@ function PageWrapper({ Component, meta }: { Component: React.ComponentType; meta
|
|
|
74
74
|
return (
|
|
75
75
|
<>
|
|
76
76
|
{meta.frontmatter && Object.keys(meta.frontmatter).length > 0 && (
|
|
77
|
-
<MetadataBlock
|
|
78
|
-
frontmatter={meta.frontmatter}
|
|
79
|
-
title={meta.title}
|
|
80
|
-
description={meta.description}
|
|
81
|
-
/>
|
|
77
|
+
<MetadataBlock frontmatter={meta.frontmatter} />
|
|
82
78
|
)}
|
|
83
79
|
<Component />
|
|
84
80
|
</>
|
package/src/theme/styles.css
CHANGED
|
@@ -165,6 +165,8 @@ body {
|
|
|
165
165
|
.sidebar-footer {
|
|
166
166
|
padding: 1rem;
|
|
167
167
|
border-top: 1px solid var(--fd-border);
|
|
168
|
+
display: flex;
|
|
169
|
+
gap: 0.5rem;
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
.theme-toggle {
|
|
@@ -597,25 +599,7 @@ body {
|
|
|
597
599
|
=========================================== */
|
|
598
600
|
|
|
599
601
|
.metadata-block {
|
|
600
|
-
margin-bottom:
|
|
601
|
-
padding-bottom: 1.5rem;
|
|
602
|
-
border-bottom: 1px solid var(--fd-border);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
.metadata-title {
|
|
606
|
-
font-size: 2.25rem;
|
|
607
|
-
font-weight: 700;
|
|
608
|
-
margin: 0 0 0.5rem 0;
|
|
609
|
-
line-height: 1.2;
|
|
610
|
-
letter-spacing: -0.025em;
|
|
611
|
-
color: var(--fd-foreground);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
.metadata-description {
|
|
615
|
-
font-size: 1.125rem;
|
|
616
|
-
color: var(--fd-muted-foreground);
|
|
617
|
-
margin: 0 0 1rem 0;
|
|
618
|
-
line-height: 1.5;
|
|
602
|
+
margin-bottom: 1.5rem;
|
|
619
603
|
}
|
|
620
604
|
|
|
621
605
|
.metadata-fields {
|
|
@@ -697,3 +681,54 @@ body {
|
|
|
697
681
|
.metadata-number {
|
|
698
682
|
font-variant-numeric: tabular-nums;
|
|
699
683
|
}
|
|
684
|
+
|
|
685
|
+
/* ===========================================
|
|
686
|
+
Full Width Mode
|
|
687
|
+
=========================================== */
|
|
688
|
+
|
|
689
|
+
.full-width .prev-content {
|
|
690
|
+
max-width: none;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* ===========================================
|
|
694
|
+
Drag and Drop Sidebar
|
|
695
|
+
=========================================== */
|
|
696
|
+
|
|
697
|
+
.sidebar-item-wrapper {
|
|
698
|
+
position: relative;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.drag-handle {
|
|
702
|
+
opacity: 0;
|
|
703
|
+
cursor: grab;
|
|
704
|
+
color: var(--fd-muted-foreground);
|
|
705
|
+
font-size: 0.75rem;
|
|
706
|
+
margin-right: 0.25rem;
|
|
707
|
+
transition: opacity 0.15s ease;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.sidebar-folder:hover .drag-handle,
|
|
711
|
+
.sidebar-item-wrapper:hover .drag-handle,
|
|
712
|
+
.sidebar-link:hover .drag-handle {
|
|
713
|
+
opacity: 0.5;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.drag-handle:hover {
|
|
717
|
+
opacity: 1 !important;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.sidebar-folder.drop-target,
|
|
721
|
+
.sidebar-item-wrapper.drop-target {
|
|
722
|
+
background: var(--fd-accent);
|
|
723
|
+
border-radius: 0.375rem;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.sidebar-folder[draggable="true"],
|
|
727
|
+
.sidebar-item-wrapper[draggable="true"] {
|
|
728
|
+
cursor: grab;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.sidebar-folder[draggable="true"]:active,
|
|
732
|
+
.sidebar-item-wrapper[draggable="true"]:active {
|
|
733
|
+
cursor: grabbing;
|
|
734
|
+
}
|