prev-cli 0.5.0 → 0.7.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 CHANGED
@@ -71,45 +71,104 @@ async function ensureCacheDir(rootDir) {
71
71
  import fg from "fast-glob";
72
72
  import { readFile } from "fs/promises";
73
73
  import path2 from "path";
74
+ function parseFrontmatter(content) {
75
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
76
+ const match = content.match(frontmatterRegex);
77
+ if (!match) {
78
+ return { frontmatter: {}, content };
79
+ }
80
+ const frontmatterStr = match[1];
81
+ const restContent = content.slice(match[0].length);
82
+ const frontmatter = {};
83
+ for (const line of frontmatterStr.split(`
84
+ `)) {
85
+ const colonIndex = line.indexOf(":");
86
+ if (colonIndex === -1)
87
+ continue;
88
+ const key = line.slice(0, colonIndex).trim();
89
+ let value = line.slice(colonIndex + 1).trim();
90
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
91
+ value = value.slice(1, -1);
92
+ }
93
+ if (value === "true")
94
+ value = true;
95
+ else if (value === "false")
96
+ value = false;
97
+ else if (!isNaN(Number(value)) && value !== "")
98
+ value = Number(value);
99
+ if (key) {
100
+ frontmatter[key] = value;
101
+ }
102
+ }
103
+ return { frontmatter, content: restContent };
104
+ }
105
+ function isIndexFile(basename) {
106
+ const lower = basename.toLowerCase();
107
+ return lower === "index" || lower === "readme";
108
+ }
74
109
  function fileToRoute(file) {
75
110
  const withoutExt = file.replace(/\.mdx?$/, "");
76
- if (withoutExt === "index") {
77
- return "/";
78
- }
79
- if (withoutExt.endsWith("/index")) {
80
- return "/" + withoutExt.slice(0, -6);
111
+ const basename = path2.basename(withoutExt).toLowerCase();
112
+ if (basename === "index" || basename === "readme") {
113
+ const dir = path2.dirname(withoutExt);
114
+ if (dir === ".") {
115
+ return "/";
116
+ }
117
+ return "/" + dir;
81
118
  }
82
119
  return "/" + withoutExt;
83
120
  }
84
121
  async function scanPages(rootDir) {
85
122
  const files = await fg.glob("**/*.{md,mdx}", {
86
123
  cwd: rootDir,
87
- ignore: ["node_modules/**", "dist/**", ".cache/**"]
124
+ ignore: ["node_modules/**", "dist/**", ".cache/**"],
125
+ dot: true
88
126
  });
89
- const pages = [];
127
+ const routeMap = new Map;
90
128
  for (const file of files) {
129
+ const route = fileToRoute(file);
130
+ const basename = path2.basename(file, path2.extname(file)).toLowerCase();
131
+ const priority = basename === "index" ? 1 : basename === "readme" ? 2 : 0;
132
+ const existing = routeMap.get(route);
133
+ if (!existing || priority > 0 && priority < existing.priority) {
134
+ routeMap.set(route, { file, priority });
135
+ }
136
+ }
137
+ const pages = [];
138
+ for (const { file } of routeMap.values()) {
91
139
  const fullPath = path2.join(rootDir, file);
92
- const content = await readFile(fullPath, "utf-8");
93
- const title = extractTitle(content, file);
94
- pages.push({
140
+ const rawContent = await readFile(fullPath, "utf-8");
141
+ const { frontmatter, content } = parseFrontmatter(rawContent);
142
+ const title = extractTitle(content, file, frontmatter);
143
+ const page = {
95
144
  route: fileToRoute(file),
96
145
  title,
97
146
  file
98
- });
147
+ };
148
+ if (frontmatter.description) {
149
+ page.description = frontmatter.description;
150
+ }
151
+ if (Object.keys(frontmatter).length > 0) {
152
+ page.frontmatter = frontmatter;
153
+ }
154
+ pages.push(page);
99
155
  }
100
156
  return pages.sort((a, b) => a.route.localeCompare(b.route));
101
157
  }
102
- function extractTitle(content, file) {
158
+ function extractTitle(content, file, frontmatter) {
159
+ if (frontmatter?.title && typeof frontmatter.title === "string") {
160
+ return frontmatter.title;
161
+ }
103
162
  const match = content.match(/^#\s+(.+)$/m);
104
163
  if (match) {
105
164
  return match[1].trim();
106
165
  }
107
- const basename = path2.basename(file, path2.extname(file));
108
- if (basename === "index") {
166
+ const basename = path2.basename(file, path2.extname(file)).toLowerCase();
167
+ if (isIndexFile(basename)) {
109
168
  const dirname = path2.dirname(file);
110
169
  return dirname === "." ? "Home" : capitalize(path2.basename(dirname));
111
170
  }
112
- return capitalize(basename);
171
+ return capitalize(path2.basename(file, path2.extname(file)));
113
172
  }
114
173
  function capitalize(str) {
115
174
  return str.charAt(0).toUpperCase() + str.slice(1).replace(/-/g, " ");
@@ -1,13 +1,27 @@
1
+ export interface Frontmatter {
2
+ title?: string;
3
+ description?: string;
4
+ [key: string]: unknown;
5
+ }
1
6
  export interface Page {
2
7
  route: string;
3
8
  title: string;
4
9
  file: string;
10
+ description?: string;
11
+ frontmatter?: Frontmatter;
5
12
  }
6
13
  export interface SidebarItem {
7
14
  title: string;
8
15
  route?: string;
9
16
  children?: SidebarItem[];
10
17
  }
18
+ /**
19
+ * Parse YAML frontmatter from markdown content
20
+ */
21
+ export declare function parseFrontmatter(content: string): {
22
+ frontmatter: Frontmatter;
23
+ content: string;
24
+ };
11
25
  export declare function fileToRoute(file: string): string;
12
26
  export declare function scanPages(rootDir: string): Promise<Page[]>;
13
27
  export declare function buildSidebarTree(pages: Page[]): SidebarItem[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
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,195 @@
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
+ container.appendChild(controls)
73
+ }
74
+
75
+ function openFullscreenModal(svgHtml: string): void {
76
+ // Remove existing modal if any
77
+ document.querySelector('.diagram-modal')?.remove()
78
+
79
+ const modal = document.createElement('div')
80
+ modal.className = 'diagram-modal'
81
+
82
+ let scale = 1
83
+ let translateX = 0
84
+ let translateY = 0
85
+ let isDragging = false
86
+ let startX = 0
87
+ let startY = 0
88
+
89
+ modal.innerHTML = `
90
+ <div class="diagram-modal-backdrop"></div>
91
+ <div class="diagram-modal-content">
92
+ <div class="diagram-modal-controls">
93
+ <button class="diagram-btn modal-zoom-in" title="Zoom in">
94
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
95
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M11 8v6M8 11h6"/>
96
+ </svg>
97
+ </button>
98
+ <button class="diagram-btn modal-zoom-out" title="Zoom out">
99
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
100
+ <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35M8 11h6"/>
101
+ </svg>
102
+ </button>
103
+ <button class="diagram-btn modal-reset" title="Reset view">
104
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
105
+ <path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"/><path d="M3 3v5h5"/>
106
+ </svg>
107
+ </button>
108
+ <button class="diagram-btn modal-close" title="Close">
109
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
110
+ <path d="M18 6L6 18M6 6l12 12"/>
111
+ </svg>
112
+ </button>
113
+ </div>
114
+ <div class="diagram-modal-svg-container">
115
+ ${svgHtml}
116
+ </div>
117
+ </div>
118
+ `
119
+
120
+ const svgContainer = modal.querySelector('.diagram-modal-svg-container') as HTMLElement
121
+ const svg = svgContainer?.querySelector('svg') as SVGElement
122
+
123
+ const updateTransform = () => {
124
+ if (svg) {
125
+ svg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`
126
+ svg.style.transformOrigin = 'center center'
127
+ }
128
+ }
129
+
130
+ // Zoom controls
131
+ modal.querySelector('.modal-zoom-in')?.addEventListener('click', () => {
132
+ scale = Math.min(scale + 0.25, 5)
133
+ updateTransform()
134
+ })
135
+
136
+ modal.querySelector('.modal-zoom-out')?.addEventListener('click', () => {
137
+ scale = Math.max(scale - 0.25, 0.25)
138
+ updateTransform()
139
+ })
140
+
141
+ modal.querySelector('.modal-reset')?.addEventListener('click', () => {
142
+ scale = 1
143
+ translateX = 0
144
+ translateY = 0
145
+ updateTransform()
146
+ })
147
+
148
+ // Close handlers
149
+ const closeModal = () => modal.remove()
150
+ modal.querySelector('.modal-close')?.addEventListener('click', closeModal)
151
+ modal.querySelector('.diagram-modal-backdrop')?.addEventListener('click', closeModal)
152
+
153
+ // Keyboard close
154
+ const handleKeydown = (e: KeyboardEvent) => {
155
+ if (e.key === 'Escape') {
156
+ closeModal()
157
+ document.removeEventListener('keydown', handleKeydown)
158
+ }
159
+ }
160
+ document.addEventListener('keydown', handleKeydown)
161
+
162
+ // Pan with mouse drag
163
+ svgContainer?.addEventListener('mousedown', (e) => {
164
+ isDragging = true
165
+ startX = e.clientX - translateX
166
+ startY = e.clientY - translateY
167
+ svgContainer.style.cursor = 'grabbing'
168
+ })
169
+
170
+ document.addEventListener('mousemove', (e) => {
171
+ if (!isDragging) return
172
+ translateX = e.clientX - startX
173
+ translateY = e.clientY - startY
174
+ updateTransform()
175
+ })
176
+
177
+ document.addEventListener('mouseup', () => {
178
+ isDragging = false
179
+ if (svgContainer) svgContainer.style.cursor = 'grab'
180
+ })
181
+
182
+ // Mouse wheel zoom
183
+ svgContainer?.addEventListener('wheel', (e) => {
184
+ e.preventDefault()
185
+ const delta = e.deltaY > 0 ? -0.1 : 0.1
186
+ scale = Math.min(Math.max(scale + delta, 0.25), 5)
187
+ updateTransform()
188
+ })
189
+
190
+ document.body.appendChild(modal)
191
+ }
192
+
4
193
  // Simple hash function for cache keys
5
194
  function hashCode(str: string): string {
6
195
  let hash = 0
@@ -67,6 +256,7 @@ async function renderMermaidDiagrams() {
67
256
  container.innerHTML = cached
68
257
  pre.style.display = 'none'
69
258
  pre.insertAdjacentElement('afterend', container)
259
+ createDiagramControls(container)
70
260
  continue
71
261
  }
72
262
 
@@ -82,6 +272,7 @@ async function renderMermaidDiagrams() {
82
272
  cacheSvg('mermaid', code, svg)
83
273
  pre.style.display = 'none'
84
274
  pre.insertAdjacentElement('afterend', container)
275
+ createDiagramControls(container)
85
276
  } catch (e) {
86
277
  console.error('Mermaid render error:', e)
87
278
  }
@@ -110,6 +301,7 @@ async function renderD2Diagrams() {
110
301
  container.innerHTML = cached
111
302
  pre.style.display = 'none'
112
303
  pre.insertAdjacentElement('afterend', container)
304
+ createDiagramControls(container)
113
305
  continue
114
306
  }
115
307
 
@@ -132,6 +324,7 @@ async function renderD2Diagrams() {
132
324
  cacheSvg('d2', code, svg)
133
325
  pre.style.display = 'none'
134
326
  pre.insertAdjacentElement('afterend', container)
327
+ createDiagramControls(container)
135
328
  } catch (e) {
136
329
  console.error('D2 render error:', e)
137
330
  }
@@ -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,114 @@ 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
+ }
353
+
354
+ .diagram-modal-svg-container svg {
355
+ max-width: none;
356
+ max-height: none;
357
+ transition: transform 0.1s ease;
187
358
  }
188
359
 
189
360
  /* Smooth page transitions */