prev-cli 0.6.0 → 0.7.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/dist/cli.js CHANGED
@@ -121,7 +121,8 @@ function fileToRoute(file) {
121
121
  async function scanPages(rootDir) {
122
122
  const files = await fg.glob("**/*.{md,mdx}", {
123
123
  cwd: rootDir,
124
- ignore: ["node_modules/**", "dist/**", ".cache/**"]
124
+ ignore: ["node_modules/**", "dist/**", ".cache/**"],
125
+ dot: true
125
126
  });
126
127
  const routeMap = new Map;
127
128
  for (const file of files) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React, { useState, useEffect } 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,40 @@ interface LayoutProps {
7
7
  children: React.ReactNode
8
8
  }
9
9
 
10
+ function SidebarToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
11
+ return (
12
+ <button
13
+ className="sidebar-toggle"
14
+ onClick={onToggle}
15
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
16
+ >
17
+ {collapsed ? (
18
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
19
+ <path d="M3 12h18M3 6h18M3 18h18" />
20
+ </svg>
21
+ ) : (
22
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
23
+ <path d="M11 19l-7-7 7-7M18 19l-7-7 7-7" />
24
+ </svg>
25
+ )}
26
+ </button>
27
+ )
28
+ }
29
+
30
+ function CollapsedFolderIcon({ item, onClick }: { item: PageTree.Folder; onClick: () => void }) {
31
+ return (
32
+ <button
33
+ className="sidebar-rail-icon"
34
+ onClick={onClick}
35
+ title={item.name}
36
+ >
37
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
38
+ <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
39
+ </svg>
40
+ </button>
41
+ )
42
+ }
43
+
10
44
  interface SidebarItemProps {
11
45
  item: PageTree.Item | PageTree.Folder
12
46
  depth?: number
@@ -88,25 +122,61 @@ function ThemeToggle() {
88
122
  }
89
123
 
90
124
  export function Layout({ tree, children }: LayoutProps) {
125
+ const [collapsed, setCollapsed] = useState(() => {
126
+ if (typeof window !== 'undefined') {
127
+ return localStorage.getItem('sidebar-collapsed') === 'true'
128
+ }
129
+ return false
130
+ })
131
+
91
132
  // Initialize theme from localStorage
92
- React.useEffect(() => {
133
+ useEffect(() => {
93
134
  const saved = localStorage.getItem('theme')
94
135
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
95
136
  const isDark = saved === 'dark' || (!saved && prefersDark)
96
137
  document.documentElement.classList.toggle('dark', isDark)
97
138
  }, [])
98
139
 
140
+ // Persist collapsed state
141
+ useEffect(() => {
142
+ localStorage.setItem('sidebar-collapsed', String(collapsed))
143
+ }, [collapsed])
144
+
145
+ const toggleCollapsed = () => setCollapsed(!collapsed)
146
+
147
+ // Get top-level folders for collapsed rail
148
+ const topFolders = tree.children.filter(
149
+ (item): item is PageTree.Folder => item.type === 'folder'
150
+ )
151
+
99
152
  return (
100
- <div className="prev-layout">
153
+ <div className={`prev-layout ${collapsed ? 'sidebar-collapsed' : ''}`}>
101
154
  <aside className="prev-sidebar">
102
- <nav className="sidebar-nav">
103
- {tree.children.map((item, i) => (
104
- <SidebarItem key={i} item={item} />
105
- ))}
106
- </nav>
107
- <div className="sidebar-footer">
108
- <ThemeToggle />
155
+ <div className="sidebar-header">
156
+ <SidebarToggle collapsed={collapsed} onToggle={toggleCollapsed} />
109
157
  </div>
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
+ ))}
167
+ </div>
168
+ ) : (
169
+ <>
170
+ <nav className="sidebar-nav">
171
+ {tree.children.map((item, i) => (
172
+ <SidebarItem key={i} item={item} />
173
+ ))}
174
+ </nav>
175
+ <div className="sidebar-footer">
176
+ <ThemeToggle />
177
+ </div>
178
+ </>
179
+ )}
110
180
  </aside>
111
181
  <main className="prev-main">
112
182
  {children}
@@ -1,6 +1,209 @@
1
1
  import { useEffect } from 'react'
2
2
  import { useLocation } from '@tanstack/react-router'
3
3
 
4
+ // Diagram controls and fullscreen functionality
5
+ function createDiagramControls(container: HTMLElement): void {
6
+ // Skip if already has controls
7
+ if (container.querySelector('.diagram-controls')) return
8
+
9
+ const wrapper = document.createElement('div')
10
+ wrapper.className = 'diagram-wrapper'
11
+
12
+ // Move container content to wrapper
13
+ const svg = container.querySelector('svg')
14
+ if (!svg) return
15
+
16
+ // Create controls
17
+ const controls = document.createElement('div')
18
+ controls.className = 'diagram-controls'
19
+ controls.innerHTML = `
20
+ <button class="diagram-btn diagram-zoom-in" title="Zoom in">
21
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
22
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M11 8v6M8 11h6"/>
23
+ </svg>
24
+ </button>
25
+ <button class="diagram-btn diagram-zoom-out" title="Zoom out">
26
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
27
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M8 11h6"/>
28
+ </svg>
29
+ </button>
30
+ <button class="diagram-btn diagram-fullscreen" title="Fullscreen">
31
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
32
+ <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"/>
33
+ </svg>
34
+ </button>
35
+ `
36
+
37
+ // Track zoom level
38
+ let scale = 1
39
+ const minScale = 0.5
40
+ const maxScale = 3
41
+ const scaleStep = 0.25
42
+
43
+ const updateScale = () => {
44
+ svg.style.transform = `scale(${scale})`
45
+ svg.style.transformOrigin = 'center center'
46
+ }
47
+
48
+ // Zoom in
49
+ controls.querySelector('.diagram-zoom-in')?.addEventListener('click', (e) => {
50
+ e.stopPropagation()
51
+ if (scale < maxScale) {
52
+ scale += scaleStep
53
+ updateScale()
54
+ }
55
+ })
56
+
57
+ // Zoom out
58
+ controls.querySelector('.diagram-zoom-out')?.addEventListener('click', (e) => {
59
+ e.stopPropagation()
60
+ if (scale > minScale) {
61
+ scale -= scaleStep
62
+ updateScale()
63
+ }
64
+ })
65
+
66
+ // Fullscreen
67
+ controls.querySelector('.diagram-fullscreen')?.addEventListener('click', (e) => {
68
+ e.stopPropagation()
69
+ openFullscreenModal(svg.outerHTML)
70
+ })
71
+
72
+ // Scroll wheel zoom on container
73
+ container.addEventListener('wheel', (e) => {
74
+ e.preventDefault()
75
+ const delta = e.deltaY > 0 ? -0.1 : 0.1
76
+ scale = Math.min(Math.max(scale + delta, minScale), maxScale)
77
+ updateScale()
78
+ })
79
+
80
+ container.appendChild(controls)
81
+ }
82
+
83
+ function openFullscreenModal(svgHtml: string): void {
84
+ // Remove existing modal if any
85
+ document.querySelector('.diagram-modal')?.remove()
86
+
87
+ const modal = document.createElement('div')
88
+ modal.className = 'diagram-modal'
89
+
90
+ let scale = 1
91
+ let translateX = 0
92
+ let translateY = 0
93
+ let isDragging = false
94
+ let startX = 0
95
+ let startY = 0
96
+
97
+ modal.innerHTML = `
98
+ <div class="diagram-modal-backdrop"></div>
99
+ <div class="diagram-modal-content">
100
+ <div class="diagram-modal-controls">
101
+ <button class="diagram-btn modal-zoom-in" title="Zoom in">
102
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
103
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M11 8v6M8 11h6"/>
104
+ </svg>
105
+ </button>
106
+ <button class="diagram-btn modal-zoom-out" title="Zoom out">
107
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
108
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M8 11h6"/>
109
+ </svg>
110
+ </button>
111
+ <button class="diagram-btn modal-reset" title="Reset view">
112
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
113
+ <path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"/><path d="M3 3v5h5"/>
114
+ </svg>
115
+ </button>
116
+ <button class="diagram-btn modal-close" title="Close">
117
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
118
+ <path d="M18 6L6 18M6 6l12 12"/>
119
+ </svg>
120
+ </button>
121
+ </div>
122
+ <div class="diagram-modal-svg-container">
123
+ ${svgHtml}
124
+ </div>
125
+ </div>
126
+ `
127
+
128
+ const svgContainer = modal.querySelector('.diagram-modal-svg-container') as HTMLElement
129
+ const svg = svgContainer?.querySelector('svg') as SVGElement
130
+
131
+ const updateTransform = () => {
132
+ if (svg) {
133
+ svg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`
134
+ svg.style.transformOrigin = 'center center'
135
+ }
136
+ }
137
+
138
+ // Zoom controls
139
+ modal.querySelector('.modal-zoom-in')?.addEventListener('click', () => {
140
+ scale = Math.min(scale + 0.25, 5)
141
+ updateTransform()
142
+ })
143
+
144
+ modal.querySelector('.modal-zoom-out')?.addEventListener('click', () => {
145
+ scale = Math.max(scale - 0.25, 0.25)
146
+ updateTransform()
147
+ })
148
+
149
+ modal.querySelector('.modal-reset')?.addEventListener('click', () => {
150
+ scale = 1
151
+ translateX = 0
152
+ translateY = 0
153
+ updateTransform()
154
+ })
155
+
156
+ // Close handlers
157
+ const closeModal = () => modal.remove()
158
+ modal.querySelector('.modal-close')?.addEventListener('click', closeModal)
159
+ modal.querySelector('.diagram-modal-backdrop')?.addEventListener('click', closeModal)
160
+
161
+ // Keyboard close
162
+ const handleKeydown = (e: KeyboardEvent) => {
163
+ if (e.key === 'Escape') {
164
+ closeModal()
165
+ document.removeEventListener('keydown', handleKeydown)
166
+ }
167
+ }
168
+ document.addEventListener('keydown', handleKeydown)
169
+
170
+ // Pan with mouse drag
171
+ svgContainer?.addEventListener('mousedown', (e) => {
172
+ isDragging = true
173
+ startX = e.clientX - translateX
174
+ startY = e.clientY - translateY
175
+ svgContainer.style.cursor = 'grabbing'
176
+ svgContainer.style.userSelect = 'none'
177
+ e.preventDefault() // Prevent text selection on drag start
178
+ })
179
+
180
+ document.addEventListener('mousemove', (e) => {
181
+ if (!isDragging) return
182
+ e.preventDefault()
183
+ translateX = e.clientX - startX
184
+ translateY = e.clientY - startY
185
+ updateTransform()
186
+ })
187
+
188
+ document.addEventListener('mouseup', () => {
189
+ isDragging = false
190
+ if (svgContainer) {
191
+ svgContainer.style.cursor = 'grab'
192
+ svgContainer.style.userSelect = ''
193
+ }
194
+ })
195
+
196
+ // Mouse wheel zoom
197
+ svgContainer?.addEventListener('wheel', (e) => {
198
+ e.preventDefault()
199
+ const delta = e.deltaY > 0 ? -0.1 : 0.1
200
+ scale = Math.min(Math.max(scale + delta, 0.25), 5)
201
+ updateTransform()
202
+ })
203
+
204
+ document.body.appendChild(modal)
205
+ }
206
+
4
207
  // Simple hash function for cache keys
5
208
  function hashCode(str: string): string {
6
209
  let hash = 0
@@ -67,6 +270,7 @@ async function renderMermaidDiagrams() {
67
270
  container.innerHTML = cached
68
271
  pre.style.display = 'none'
69
272
  pre.insertAdjacentElement('afterend', container)
273
+ createDiagramControls(container)
70
274
  continue
71
275
  }
72
276
 
@@ -82,6 +286,7 @@ async function renderMermaidDiagrams() {
82
286
  cacheSvg('mermaid', code, svg)
83
287
  pre.style.display = 'none'
84
288
  pre.insertAdjacentElement('afterend', container)
289
+ createDiagramControls(container)
85
290
  } catch (e) {
86
291
  console.error('Mermaid render error:', e)
87
292
  }
@@ -110,6 +315,7 @@ async function renderD2Diagrams() {
110
315
  container.innerHTML = cached
111
316
  pre.style.display = 'none'
112
317
  pre.insertAdjacentElement('afterend', container)
318
+ createDiagramControls(container)
113
319
  continue
114
320
  }
115
321
 
@@ -132,6 +338,7 @@ async function renderD2Diagrams() {
132
338
  cacheSvg('d2', code, svg)
133
339
  pre.style.display = 'none'
134
340
  pre.insertAdjacentElement('afterend', container)
341
+ createDiagramControls(container)
135
342
  } catch (e) {
136
343
  console.error('D2 render error:', e)
137
344
  }
@@ -16,9 +16,15 @@ body {
16
16
 
17
17
  /* Layout structure */
18
18
  .prev-layout {
19
+ --sidebar-width: 260px;
19
20
  display: grid;
20
- grid-template-columns: 260px 1fr;
21
+ grid-template-columns: var(--sidebar-width) 1fr;
21
22
  min-height: 100vh;
23
+ transition: grid-template-columns 0.2s ease;
24
+ }
25
+
26
+ .prev-layout.sidebar-collapsed {
27
+ --sidebar-width: 50px;
22
28
  }
23
29
 
24
30
  /* Sidebar */
@@ -26,11 +32,74 @@ body {
26
32
  position: sticky;
27
33
  top: 0;
28
34
  height: 100vh;
35
+ width: var(--sidebar-width);
29
36
  background: var(--fd-background);
30
37
  border-right: 1px solid var(--fd-border);
31
38
  display: flex;
32
39
  flex-direction: column;
33
40
  overflow: hidden;
41
+ transition: width 0.2s ease;
42
+ }
43
+
44
+ /* Sidebar header with toggle */
45
+ .sidebar-header {
46
+ padding: 0.75rem;
47
+ border-bottom: 1px solid var(--fd-border);
48
+ display: flex;
49
+ justify-content: flex-start;
50
+ }
51
+
52
+ .sidebar-collapsed .sidebar-header {
53
+ justify-content: center;
54
+ }
55
+
56
+ .sidebar-toggle {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ width: 36px;
61
+ height: 36px;
62
+ background: var(--fd-muted);
63
+ border: none;
64
+ border-radius: 0.5rem;
65
+ color: var(--fd-muted-foreground);
66
+ cursor: pointer;
67
+ transition: all 0.15s ease;
68
+ }
69
+
70
+ .sidebar-toggle:hover {
71
+ background: var(--fd-accent);
72
+ color: var(--fd-accent-foreground);
73
+ }
74
+
75
+ /* Collapsed rail */
76
+ .sidebar-rail {
77
+ display: flex;
78
+ flex-direction: column;
79
+ align-items: center;
80
+ padding: 0.5rem;
81
+ gap: 0.5rem;
82
+ flex: 1;
83
+ overflow-y: auto;
84
+ }
85
+
86
+ .sidebar-rail-icon {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ width: 36px;
91
+ height: 36px;
92
+ background: transparent;
93
+ border: none;
94
+ border-radius: 0.5rem;
95
+ color: var(--fd-muted-foreground);
96
+ cursor: pointer;
97
+ transition: all 0.15s ease;
98
+ }
99
+
100
+ .sidebar-rail-icon:hover {
101
+ background: var(--fd-muted);
102
+ color: var(--fd-foreground);
34
103
  }
35
104
 
36
105
  .sidebar-nav {
@@ -178,12 +247,116 @@ body {
178
247
  border-radius: 0.5rem;
179
248
  display: flex;
180
249
  justify-content: center;
250
+ position: relative;
251
+ overflow: hidden;
181
252
  }
182
253
 
183
254
  .mermaid-diagram svg,
184
255
  .d2-diagram svg {
185
256
  max-width: 100%;
186
257
  height: auto;
258
+ transition: transform 0.2s ease;
259
+ }
260
+
261
+ /* Diagram hover controls */
262
+ .diagram-controls {
263
+ position: absolute;
264
+ top: 0.5rem;
265
+ right: 0.5rem;
266
+ display: flex;
267
+ gap: 0.25rem;
268
+ opacity: 0;
269
+ transition: opacity 0.15s ease;
270
+ z-index: 10;
271
+ }
272
+
273
+ .mermaid-diagram:hover .diagram-controls,
274
+ .d2-diagram:hover .diagram-controls {
275
+ opacity: 1;
276
+ }
277
+
278
+ .diagram-btn {
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ width: 32px;
283
+ height: 32px;
284
+ background: var(--fd-background);
285
+ border: 1px solid var(--fd-border);
286
+ border-radius: 0.375rem;
287
+ color: var(--fd-muted-foreground);
288
+ cursor: pointer;
289
+ transition: all 0.15s ease;
290
+ }
291
+
292
+ .diagram-btn:hover {
293
+ background: var(--fd-accent);
294
+ color: var(--fd-accent-foreground);
295
+ border-color: var(--fd-accent);
296
+ }
297
+
298
+ /* Fullscreen modal */
299
+ .diagram-modal {
300
+ position: fixed;
301
+ inset: 0;
302
+ z-index: 9999;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ }
307
+
308
+ .diagram-modal-backdrop {
309
+ position: absolute;
310
+ inset: 0;
311
+ background: rgba(0, 0, 0, 0.8);
312
+ backdrop-filter: blur(4px);
313
+ }
314
+
315
+ .diagram-modal-content {
316
+ position: relative;
317
+ width: 90vw;
318
+ height: 90vh;
319
+ background: var(--fd-background);
320
+ border-radius: 0.75rem;
321
+ display: flex;
322
+ flex-direction: column;
323
+ overflow: hidden;
324
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
325
+ }
326
+
327
+ .diagram-modal-controls {
328
+ display: flex;
329
+ gap: 0.5rem;
330
+ padding: 0.75rem;
331
+ border-bottom: 1px solid var(--fd-border);
332
+ background: var(--fd-muted);
333
+ }
334
+
335
+ .diagram-modal-controls .diagram-btn {
336
+ width: 36px;
337
+ height: 36px;
338
+ }
339
+
340
+ .diagram-modal-controls .modal-close {
341
+ margin-left: auto;
342
+ }
343
+
344
+ .diagram-modal-svg-container {
345
+ flex: 1;
346
+ overflow: hidden;
347
+ display: flex;
348
+ align-items: center;
349
+ justify-content: center;
350
+ cursor: grab;
351
+ padding: 2rem;
352
+ -webkit-user-select: none;
353
+ user-select: none;
354
+ }
355
+
356
+ .diagram-modal-svg-container svg {
357
+ max-width: none;
358
+ max-height: none;
359
+ transition: transform 0.1s ease;
187
360
  }
188
361
 
189
362
  /* Smooth page transitions */