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 +74 -15
- package/dist/vite/pages.d.ts +14 -0
- package/package.json +1 -1
- package/src/theme/Layout.tsx +80 -10
- package/src/theme/diagrams.tsx +193 -0
- package/src/theme/styles.css +172 -1
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
93
|
-
const
|
|
94
|
-
|
|
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
|
|
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, " ");
|
package/dist/vite/pages.d.ts
CHANGED
|
@@ -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
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,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
|
}
|
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,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 */
|