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 +2 -1
- package/package.json +1 -1
- package/src/theme/Layout.tsx +80 -10
- package/src/theme/diagrams.tsx +207 -0
- package/src/theme/styles.css +174 -1
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
package/src/theme/Layout.tsx
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
153
|
+
<div className={`prev-layout ${collapsed ? 'sidebar-collapsed' : ''}`}>
|
|
101
154
|
<aside className="prev-sidebar">
|
|
102
|
-
<
|
|
103
|
-
{
|
|
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}
|
package/src/theme/diagrams.tsx
CHANGED
|
@@ -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
|
}
|
package/src/theme/styles.css
CHANGED
|
@@ -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:
|
|
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 */
|