@docmd/ui 0.4.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/LICENSE +21 -0
- package/README.md +8 -0
- package/assets/css/docmd-highlight-dark.css +86 -0
- package/assets/css/docmd-highlight-light.css +86 -0
- package/assets/css/docmd-main.css +1720 -0
- package/assets/favicon.ico +0 -0
- package/assets/images/docmd-logo-dark.png +0 -0
- package/assets/images/docmd-logo-light.png +0 -0
- package/assets/js/docmd-image-lightbox.js +74 -0
- package/assets/js/docmd-main.js +276 -0
- package/index.js +11 -0
- package/package.json +12 -0
- package/templates/layout.ejs +195 -0
- package/templates/navigation.ejs +92 -0
- package/templates/no-style.ejs +195 -0
- package/templates/partials/theme-init.js +34 -0
- package/templates/toc.ejs +38 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A simple lightbox implementation for gallery images
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
8
|
+
// Create lightbox elements
|
|
9
|
+
const lightbox = document.createElement('div');
|
|
10
|
+
lightbox.className = 'docmd-lightbox';
|
|
11
|
+
lightbox.innerHTML = `
|
|
12
|
+
<div class="docmd-lightbox-content">
|
|
13
|
+
<img src="" alt="">
|
|
14
|
+
<div class="docmd-lightbox-caption"></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="docmd-lightbox-close">×</div>
|
|
17
|
+
`;
|
|
18
|
+
document.body.appendChild(lightbox);
|
|
19
|
+
|
|
20
|
+
const lightboxImg = lightbox.querySelector('img');
|
|
21
|
+
const lightboxCaption = lightbox.querySelector('.docmd-lightbox-caption');
|
|
22
|
+
const lightboxClose = lightbox.querySelector('.docmd-lightbox-close');
|
|
23
|
+
|
|
24
|
+
// Find all images with lightbox class or in image galleries
|
|
25
|
+
const lightboxImages = document.querySelectorAll('img.lightbox, .image-gallery img');
|
|
26
|
+
|
|
27
|
+
// Add click event to each image
|
|
28
|
+
lightboxImages.forEach(function (img) {
|
|
29
|
+
img.style.cursor = 'zoom-in';
|
|
30
|
+
|
|
31
|
+
img.addEventListener('click', function () {
|
|
32
|
+
// Get the image source and caption
|
|
33
|
+
const src = this.getAttribute('src');
|
|
34
|
+
let caption = this.getAttribute('alt') || '';
|
|
35
|
+
|
|
36
|
+
// If image is inside a figure with figcaption, use that caption
|
|
37
|
+
const figure = this.closest('figure');
|
|
38
|
+
if (figure) {
|
|
39
|
+
const figcaption = figure.querySelector('figcaption');
|
|
40
|
+
if (figcaption) {
|
|
41
|
+
caption = figcaption.textContent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Set the lightbox content
|
|
46
|
+
lightboxImg.setAttribute('src', src);
|
|
47
|
+
lightboxCaption.textContent = caption;
|
|
48
|
+
|
|
49
|
+
// Show the lightbox
|
|
50
|
+
lightbox.style.display = 'flex';
|
|
51
|
+
document.body.style.overflow = 'hidden'; // Prevent scrolling
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Close lightbox when clicking the close button or outside the image
|
|
56
|
+
lightboxClose.addEventListener('click', closeLightbox);
|
|
57
|
+
lightbox.addEventListener('click', function (e) {
|
|
58
|
+
if (e.target === lightbox) {
|
|
59
|
+
closeLightbox();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Close lightbox when pressing Escape key
|
|
64
|
+
document.addEventListener('keydown', function (e) {
|
|
65
|
+
if (e.key === 'Escape' && lightbox.style.display === 'flex') {
|
|
66
|
+
closeLightbox();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function closeLightbox() {
|
|
71
|
+
lightbox.style.display = 'none';
|
|
72
|
+
document.body.style.overflow = ''; // Restore scrolling
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Main client-side script for docmd UI interactions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// --- Collapsible Navigation Logic ---
|
|
8
|
+
function initializeCollapsibleNav() {
|
|
9
|
+
const nav = document.querySelector('.sidebar-nav');
|
|
10
|
+
if (!nav) return;
|
|
11
|
+
|
|
12
|
+
// 1. Initial Cleanup (ensure classes match aria states)
|
|
13
|
+
nav.querySelectorAll('li.collapsible').forEach(item => {
|
|
14
|
+
// If server rendered it as expanded, ensure aria matches
|
|
15
|
+
if (item.classList.contains('expanded')) {
|
|
16
|
+
item.setAttribute('aria-expanded', 'true');
|
|
17
|
+
} else {
|
|
18
|
+
item.setAttribute('aria-expanded', 'false');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 2. Event Delegation
|
|
23
|
+
nav.addEventListener('click', (e) => {
|
|
24
|
+
// Check if the click target is the arrow or inside the arrow wrapper
|
|
25
|
+
const arrow = e.target.closest('.collapse-icon-wrapper');
|
|
26
|
+
|
|
27
|
+
if (arrow) {
|
|
28
|
+
// STOP everything. Do not follow the link.
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
|
|
32
|
+
const item = arrow.closest('li.collapsible');
|
|
33
|
+
if (item) {
|
|
34
|
+
// Toggle State
|
|
35
|
+
const isExpanded = item.classList.contains('expanded');
|
|
36
|
+
|
|
37
|
+
if (isExpanded) {
|
|
38
|
+
item.classList.remove('expanded');
|
|
39
|
+
item.setAttribute('aria-expanded', 'false');
|
|
40
|
+
} else {
|
|
41
|
+
item.classList.add('expanded');
|
|
42
|
+
item.setAttribute('aria-expanded', 'true');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Mobile Menu Logic ---
|
|
51
|
+
function initializeMobileMenus() {
|
|
52
|
+
const sidebarBtn = document.querySelector('.sidebar-menu-button');
|
|
53
|
+
const sidebar = document.querySelector('.sidebar');
|
|
54
|
+
|
|
55
|
+
if (sidebarBtn && sidebar) {
|
|
56
|
+
sidebarBtn.addEventListener('click', (e) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
sidebar.classList.toggle('mobile-expanded');
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tocBtn = document.querySelector('.toc-menu-button');
|
|
63
|
+
const tocContainer = document.querySelector('.toc-container');
|
|
64
|
+
const tocTitle = document.querySelector('.toc-title');
|
|
65
|
+
|
|
66
|
+
const toggleToc = (e) => {
|
|
67
|
+
if (window.getComputedStyle(tocBtn).display === 'none') return;
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
tocContainer.classList.toggle('mobile-expanded');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (tocBtn && tocContainer) {
|
|
73
|
+
tocBtn.addEventListener('click', toggleToc);
|
|
74
|
+
if (tocTitle) tocTitle.addEventListener('click', toggleToc);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Sidebar Scroll Preservation (Instant Center) ---
|
|
79
|
+
function initializeSidebarScroll() {
|
|
80
|
+
const sidebar = document.querySelector('.sidebar');
|
|
81
|
+
if (!sidebar) return;
|
|
82
|
+
|
|
83
|
+
// Wait for the layout to be stable
|
|
84
|
+
requestAnimationFrame(() => {
|
|
85
|
+
// Find the active link
|
|
86
|
+
const activeElement = sidebar.querySelector('a.active');
|
|
87
|
+
|
|
88
|
+
if (activeElement) {
|
|
89
|
+
activeElement.scrollIntoView({
|
|
90
|
+
behavior: 'auto', // INSTANT jump (prevents scrolling animation jitter)
|
|
91
|
+
block: 'center', // Center it vertically in the sidebar
|
|
92
|
+
inline: 'nearest'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Theme Toggle Logic ---
|
|
99
|
+
function setupThemeToggleListener() {
|
|
100
|
+
const toggleButtons = document.querySelectorAll('.theme-toggle-button');
|
|
101
|
+
|
|
102
|
+
function applyTheme(theme) {
|
|
103
|
+
const validThemes = ['light', 'dark'];
|
|
104
|
+
const selectedTheme = validThemes.includes(theme) ? theme : 'light';
|
|
105
|
+
|
|
106
|
+
// 1. Update DOM & Storage
|
|
107
|
+
document.documentElement.setAttribute('data-theme', selectedTheme);
|
|
108
|
+
document.body.setAttribute('data-theme', selectedTheme);
|
|
109
|
+
localStorage.setItem('docmd-theme', selectedTheme);
|
|
110
|
+
|
|
111
|
+
// 2. Toggle Highlight.js Stylesheets
|
|
112
|
+
const lightLink = document.getElementById('hljs-light');
|
|
113
|
+
const darkLink = document.getElementById('hljs-dark');
|
|
114
|
+
|
|
115
|
+
if (lightLink && darkLink) {
|
|
116
|
+
if (selectedTheme === 'dark') {
|
|
117
|
+
lightLink.disabled = true;
|
|
118
|
+
darkLink.disabled = false;
|
|
119
|
+
} else {
|
|
120
|
+
lightLink.disabled = false;
|
|
121
|
+
darkLink.disabled = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
toggleButtons.forEach(btn => {
|
|
127
|
+
// Clone button to remove old listeners if any (safety measure)
|
|
128
|
+
const newBtn = btn.cloneNode(true);
|
|
129
|
+
btn.parentNode.replaceChild(newBtn, btn);
|
|
130
|
+
|
|
131
|
+
newBtn.addEventListener('click', () => {
|
|
132
|
+
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
133
|
+
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
134
|
+
applyTheme(newTheme);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Sidebar Collapse Logic ---
|
|
140
|
+
function initializeSidebarToggle() {
|
|
141
|
+
const toggleButton = document.getElementById('sidebar-toggle-button');
|
|
142
|
+
const body = document.body;
|
|
143
|
+
|
|
144
|
+
if (!body.classList.contains('sidebar-collapsible') || !toggleButton) return;
|
|
145
|
+
|
|
146
|
+
const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
|
|
147
|
+
let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
|
|
148
|
+
|
|
149
|
+
if (isCollapsed === null) isCollapsed = defaultConfigCollapsed;
|
|
150
|
+
else isCollapsed = isCollapsed === 'true';
|
|
151
|
+
|
|
152
|
+
if (isCollapsed) body.classList.add('sidebar-collapsed');
|
|
153
|
+
|
|
154
|
+
toggleButton.addEventListener('click', () => {
|
|
155
|
+
body.classList.toggle('sidebar-collapsed');
|
|
156
|
+
const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
|
|
157
|
+
localStorage.setItem('docmd-sidebar-collapsed', currentlyCollapsed);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Tabs Container Logic ---
|
|
162
|
+
function initializeTabs() {
|
|
163
|
+
document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
|
|
164
|
+
const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
|
|
165
|
+
const tabPanes = tabsContainer.querySelectorAll('.docmd-tab-pane');
|
|
166
|
+
|
|
167
|
+
navItems.forEach((navItem, index) => {
|
|
168
|
+
navItem.addEventListener('click', () => {
|
|
169
|
+
navItems.forEach(item => item.classList.remove('active'));
|
|
170
|
+
tabPanes.forEach(pane => pane.classList.remove('active'));
|
|
171
|
+
|
|
172
|
+
navItem.classList.add('active');
|
|
173
|
+
if (tabPanes[index]) tabPanes[index].classList.add('active');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Copy Code Button Logic ---
|
|
180
|
+
function initializeCopyCodeButtons() {
|
|
181
|
+
if (document.body.dataset.copyCodeEnabled !== 'true') return;
|
|
182
|
+
|
|
183
|
+
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
|
|
184
|
+
const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
|
185
|
+
|
|
186
|
+
document.querySelectorAll('pre').forEach(preElement => {
|
|
187
|
+
const codeElement = preElement.querySelector('code');
|
|
188
|
+
if (!codeElement) return;
|
|
189
|
+
|
|
190
|
+
const wrapper = document.createElement('div');
|
|
191
|
+
wrapper.style.position = 'relative';
|
|
192
|
+
wrapper.style.display = 'block';
|
|
193
|
+
|
|
194
|
+
preElement.parentNode.insertBefore(wrapper, preElement);
|
|
195
|
+
wrapper.appendChild(preElement);
|
|
196
|
+
preElement.style.position = 'static';
|
|
197
|
+
|
|
198
|
+
const copyButton = document.createElement('button');
|
|
199
|
+
copyButton.className = 'copy-code-button';
|
|
200
|
+
copyButton.innerHTML = copyIconSvg;
|
|
201
|
+
copyButton.setAttribute('aria-label', 'Copy code to clipboard');
|
|
202
|
+
wrapper.appendChild(copyButton);
|
|
203
|
+
|
|
204
|
+
copyButton.addEventListener('click', () => {
|
|
205
|
+
navigator.clipboard.writeText(codeElement.innerText).then(() => {
|
|
206
|
+
copyButton.innerHTML = checkIconSvg;
|
|
207
|
+
copyButton.classList.add('copied');
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
copyButton.innerHTML = copyIconSvg;
|
|
210
|
+
copyButton.classList.remove('copied');
|
|
211
|
+
}, 2000);
|
|
212
|
+
}).catch(err => {
|
|
213
|
+
console.error('Failed to copy text: ', err);
|
|
214
|
+
copyButton.innerText = 'Error';
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Theme Sync Function ---
|
|
221
|
+
function syncBodyTheme() {
|
|
222
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
223
|
+
if (currentTheme && document.body) {
|
|
224
|
+
document.body.setAttribute('data-theme', currentTheme);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Scroll Spy Logic ---
|
|
229
|
+
function initializeScrollSpy() {
|
|
230
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
231
|
+
const headings = document.querySelectorAll('.main-content h2, .main-content h3');
|
|
232
|
+
|
|
233
|
+
if (tocLinks.length === 0 || headings.length === 0) return;
|
|
234
|
+
|
|
235
|
+
const observerOptions = {
|
|
236
|
+
root: null,
|
|
237
|
+
// Trigger when heading crosses the top 10% of screen
|
|
238
|
+
rootMargin: '-10% 0px -80% 0px',
|
|
239
|
+
threshold: 0
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const observer = new IntersectionObserver((entries) => {
|
|
243
|
+
entries.forEach(entry => {
|
|
244
|
+
if (entry.isIntersecting) {
|
|
245
|
+
// 1. Clear current active state
|
|
246
|
+
tocLinks.forEach(link => link.classList.remove('active'));
|
|
247
|
+
|
|
248
|
+
// 2. Find link corresponding to this heading
|
|
249
|
+
const id = entry.target.getAttribute('id');
|
|
250
|
+
const activeLink = document.querySelector(`.toc-link[href="#${id}"]`);
|
|
251
|
+
|
|
252
|
+
if (activeLink) {
|
|
253
|
+
activeLink.classList.add('active');
|
|
254
|
+
|
|
255
|
+
// Optional: Auto-scroll the TOC sidebar itself if needed
|
|
256
|
+
// activeLink.scrollIntoView({ block: 'nearest' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}, observerOptions);
|
|
261
|
+
|
|
262
|
+
headings.forEach(heading => observer.observe(heading));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Main Execution ---
|
|
266
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
267
|
+
syncBodyTheme();
|
|
268
|
+
setupThemeToggleListener();
|
|
269
|
+
initializeSidebarToggle();
|
|
270
|
+
initializeTabs();
|
|
271
|
+
initializeCopyCodeButtons();
|
|
272
|
+
initializeCollapsibleNav();
|
|
273
|
+
initializeMobileMenus();
|
|
274
|
+
initializeSidebarScroll();
|
|
275
|
+
initializeScrollSpy();
|
|
276
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
getTemplatesDir: () => path.join(__dirname, 'templates'),
|
|
5
|
+
getAssetsDir: () => path.join(__dirname, 'assets'),
|
|
6
|
+
// Helper to resolve template paths
|
|
7
|
+
getTemplatePath: (name) => {
|
|
8
|
+
const fileName = name.endsWith('.ejs') ? name : `${name}.ejs`;
|
|
9
|
+
return path.join(__dirname, 'templates', fileName);
|
|
10
|
+
}
|
|
11
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<%# packages/ui/templates/layout.ejs %>
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
|
|
8
|
+
<!-- Define Globals -->
|
|
9
|
+
<script>
|
|
10
|
+
var root = "<%= relativePathToRoot %>";
|
|
11
|
+
if (root && !root.endsWith('/')) root += '/';
|
|
12
|
+
if (root === '') root = './';
|
|
13
|
+
window.DOCMD_ROOT = root;
|
|
14
|
+
window.DOCMD_DEFAULT_MODE = "<%= defaultMode %>";
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<title><%= pageTitle %></title>
|
|
18
|
+
|
|
19
|
+
<!-- Favicon -->
|
|
20
|
+
<%- faviconLinkHtml || '' %>
|
|
21
|
+
|
|
22
|
+
<!-- 1. CORE STYLES (Foundation) -->
|
|
23
|
+
<link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-main.css?v=<%= buildHash %>">
|
|
24
|
+
|
|
25
|
+
<!-- 2. HIGHLIGHT.JS (Toggle Strategy) -->
|
|
26
|
+
<% if (config.theme?.codeHighlight !== false) {
|
|
27
|
+
const isDarkDefault = defaultMode === 'dark';
|
|
28
|
+
%>
|
|
29
|
+
<link rel="stylesheet"
|
|
30
|
+
href="<%= relativePathToRoot %>assets/css/docmd-highlight-light.css?v=<%= buildHash %>"
|
|
31
|
+
id="hljs-light"
|
|
32
|
+
<%= isDarkDefault ? 'disabled' : '' %>>
|
|
33
|
+
|
|
34
|
+
<link rel="stylesheet"
|
|
35
|
+
href="<%= relativePathToRoot %>assets/css/docmd-highlight-dark.css?v=<%= buildHash %>"
|
|
36
|
+
id="hljs-dark"
|
|
37
|
+
<%= isDarkDefault ? '' : 'disabled' %>>
|
|
38
|
+
<% } %>
|
|
39
|
+
|
|
40
|
+
<!-- 3. PLUGINS & THEMES (Overrides Core) -->
|
|
41
|
+
<!-- This includes 'docmd-theme-sky.css' injected by build.js -->
|
|
42
|
+
<%- pluginHeadScriptsHtml || '' %>
|
|
43
|
+
|
|
44
|
+
<!-- 4. CUSTOM CSS (Overrides Everything) -->
|
|
45
|
+
<% (customCssFiles || []).forEach(cssFile => { %>
|
|
46
|
+
<link rel="stylesheet" href="<%= relativePathToRoot %><%- cssFile.startsWith('/') ? cssFile.substring(1) : cssFile %>?v=<%= buildHash %>">
|
|
47
|
+
<% }); %>
|
|
48
|
+
|
|
49
|
+
<!-- Theme Init Script (Must be after styles to prevent FOUC) -->
|
|
50
|
+
<%- themeInitScript %>
|
|
51
|
+
</head>
|
|
52
|
+
<body class="<%= sidebarConfig.collapsible ? 'sidebar-collapsible' : 'sidebar-not-collapsible' %>"
|
|
53
|
+
data-default-collapsed="<%= sidebarConfig.defaultCollapsed %>"
|
|
54
|
+
data-copy-code-enabled="<%= config.copyCode === true %>">
|
|
55
|
+
|
|
56
|
+
<aside class="sidebar">
|
|
57
|
+
<div class="sidebar-header">
|
|
58
|
+
<% if (logo && logo.light && logo.dark) { %>
|
|
59
|
+
<a href="<%= logo.href || relativePathToRoot %>" class="logo-link">
|
|
60
|
+
<img src="<%= relativePathToRoot %><%- logo.light.startsWith('/') ? logo.light.substring(1) : logo.light %>" alt="<%= logo.alt || siteTitle %>" class="logo-light" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
|
|
61
|
+
<img src="<%= relativePathToRoot %><%- logo.dark.startsWith('/') ? logo.dark.substring(1) : logo.dark %>" alt="<%= logo.alt || siteTitle %>" class="logo-dark" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
|
|
62
|
+
</a>
|
|
63
|
+
<% } else { %>
|
|
64
|
+
<h1><a href="<%= relativePathToRoot %>index.html"><%= siteTitle %></a></h1>
|
|
65
|
+
<% } %>
|
|
66
|
+
<span class="mobile-view sidebar-menu-button float-right">
|
|
67
|
+
<%- renderIcon("ellipsis-vertical") %>
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<%- navigationHtml %>
|
|
72
|
+
|
|
73
|
+
<% if (theme && theme.enableModeToggle && theme.positionMode !== 'top') { %>
|
|
74
|
+
<button id="theme-toggle-button" aria-label="Toggle theme" class="theme-toggle-button">
|
|
75
|
+
<%- renderIcon('sun', { class: 'icon-sun' }) %> <%- renderIcon('moon', { class: 'icon-moon' }) %>
|
|
76
|
+
</button>
|
|
77
|
+
<% } %>
|
|
78
|
+
</aside>
|
|
79
|
+
|
|
80
|
+
<div class="main-content-wrapper">
|
|
81
|
+
<div class="page-header">
|
|
82
|
+
<div class="header-left">
|
|
83
|
+
<% if (sidebarConfig.collapsible) { %>
|
|
84
|
+
<button id="sidebar-toggle-button" class="sidebar-toggle-button" aria-label="Toggle Sidebar">
|
|
85
|
+
<%- renderIcon('panel-left-close') %>
|
|
86
|
+
</button>
|
|
87
|
+
<% } %>
|
|
88
|
+
<h1><%= pageTitle %></h1>
|
|
89
|
+
</div>
|
|
90
|
+
<% if (theme && theme.enableModeToggle && theme.positionMode === 'top') { %>
|
|
91
|
+
<div class="header-right">
|
|
92
|
+
|
|
93
|
+
<!-- SEARCH BUTTON -->
|
|
94
|
+
<% if (config.search !== false) { %>
|
|
95
|
+
<button class="docmd-search-trigger" aria-label="Search">
|
|
96
|
+
<%- renderIcon('search') %>
|
|
97
|
+
<span class="search-label">Search</span>
|
|
98
|
+
<span class="search-keys"><kbd class="docmd-kbd">⌘</kbd><kbd class="docmd-kbd">k</kbd></span>
|
|
99
|
+
</button>
|
|
100
|
+
<% } %>
|
|
101
|
+
|
|
102
|
+
<!-- THEME TOGGLE -->
|
|
103
|
+
<button id="theme-toggle-button" aria-label="Toggle theme" class="theme-toggle-button theme-toggle-header">
|
|
104
|
+
<%- renderIcon('sun', { class: 'icon-sun' }) %> <%- renderIcon('moon', { class: 'icon-moon' }) %>
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
<!-- SPONSOR BUTTON -->
|
|
108
|
+
<% if (sponsor && sponsor.enabled) { %>
|
|
109
|
+
<a href="<%= sponsor.link %>" target="_blank" rel="noopener noreferrer" aria-label="Sponsor Project">
|
|
110
|
+
<button class="sponsor-link-button" aria-label="Sponsor">
|
|
111
|
+
<%- renderIcon('heart', { class: 'sponsor-icon' }) %>
|
|
112
|
+
</button>
|
|
113
|
+
</a>
|
|
114
|
+
<% } %>
|
|
115
|
+
</div>
|
|
116
|
+
<% } %>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<main class="content-area">
|
|
120
|
+
<div class="content-layout">
|
|
121
|
+
<div class="main-content">
|
|
122
|
+
<%- content %>
|
|
123
|
+
|
|
124
|
+
<% if (config.pageNavigation && (prevPage || nextPage)) { %>
|
|
125
|
+
<div class="page-navigation">
|
|
126
|
+
<% if (prevPage) { %>
|
|
127
|
+
<a href="<%= prevPage.url %>" class="prev-page">
|
|
128
|
+
<%- renderIcon('arrow-left', { class: 'page-nav-icon' }) %>
|
|
129
|
+
<span><small>Previous</small><strong><%= prevPage.title %></strong></span>
|
|
130
|
+
</a>
|
|
131
|
+
<% } else { %><div class="prev-page-placeholder"></div><% } %>
|
|
132
|
+
<% if (nextPage) { %>
|
|
133
|
+
<a href="<%= nextPage.url %>" class="next-page">
|
|
134
|
+
<span><small>Next</small><strong><%= nextPage.title %></strong></span>
|
|
135
|
+
<%- renderIcon('arrow-right', { class: 'page-nav-icon' }) %>
|
|
136
|
+
</a>
|
|
137
|
+
<% } else { %><div class="next-page-placeholder"></div><% } %>
|
|
138
|
+
</div>
|
|
139
|
+
<% } %>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="toc-sidebar">
|
|
143
|
+
<%- include('toc', { content, headings, navigationHtml, isActivePage }) %>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="page-footer-actions">
|
|
148
|
+
<% if (locals.editUrl) { %>
|
|
149
|
+
<a href="<%= editUrl %>" target="_blank" rel="noopener noreferrer" class="edit-link">
|
|
150
|
+
<%- renderIcon('pencil') %> <%= editLinkText %>
|
|
151
|
+
</a>
|
|
152
|
+
<% } %>
|
|
153
|
+
</div>
|
|
154
|
+
</main>
|
|
155
|
+
|
|
156
|
+
<footer class="page-footer">
|
|
157
|
+
<div class="footer-content">
|
|
158
|
+
<div class="user-footer"><%- footerHtml || '' %></div>
|
|
159
|
+
<div class="branding-footer">
|
|
160
|
+
Build with <a href="https://docmd.io" target="_blank" rel="noopener">docmd.</a>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</footer>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<% if (config.search !== false) { %>
|
|
167
|
+
<!-- Search Modal -->
|
|
168
|
+
<div id="docmd-search-modal" class="docmd-search-modal" style="display: none;">
|
|
169
|
+
<div class="docmd-search-box">
|
|
170
|
+
<div class="docmd-search-header">
|
|
171
|
+
<%- renderIcon('search') %>
|
|
172
|
+
<input type="text" id="docmd-search-input" placeholder="Search documentation..." autocomplete="off" spellcheck="false">
|
|
173
|
+
<button onclick="window.closeDocmdSearch()" class="docmd-search-close" aria-label="Close search">
|
|
174
|
+
<%- renderIcon('x') %>
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
<div id="docmd-search-results" class="docmd-search-results"></div>
|
|
178
|
+
<div class="docmd-search-footer">
|
|
179
|
+
<span><kbd class="docmd-kbd">↑</kbd> <kbd class="docmd-kbd">↓</kbd> to navigate</span>
|
|
180
|
+
<span><kbd class="docmd-kbd">ESC</kbd> to close</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<% } %>
|
|
185
|
+
|
|
186
|
+
<!-- Plugin Scripts -->
|
|
187
|
+
<%- pluginBodyScriptsHtml || '' %>
|
|
188
|
+
|
|
189
|
+
<!-- User Custom JS (Sanitized) -->
|
|
190
|
+
<% (customJsFiles || []).forEach(jsFile => {
|
|
191
|
+
if (jsFile && jsFile.trim() !== '') { %>
|
|
192
|
+
<script src="<%= relativePathToRoot %><%- jsFile.startsWith('/') ? jsFile.substring(1) : jsFile %>"></script>
|
|
193
|
+
<% } }); %>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<%# packages/ui/templates/navigation.ejs %>
|
|
2
|
+
|
|
3
|
+
<nav class="sidebar-nav" aria-label="Main navigation">
|
|
4
|
+
<ul>
|
|
5
|
+
<%
|
|
6
|
+
function normalizePath(p) {
|
|
7
|
+
if (!p || p === '#') return '#';
|
|
8
|
+
if (p.startsWith('http')) return p;
|
|
9
|
+
|
|
10
|
+
// 1. Strip dot-slash and leading slash
|
|
11
|
+
let path = p.replace(/^(\.\/|\/)+/, '');
|
|
12
|
+
|
|
13
|
+
// 2. Remove extensions and index
|
|
14
|
+
path = path.replace(/(\/index\.html|index\.html|\.html|\.md)$/, '');
|
|
15
|
+
|
|
16
|
+
// 3. CRITICAL: Remove trailing slash for consistency
|
|
17
|
+
path = path.replace(/\/$/, '');
|
|
18
|
+
|
|
19
|
+
// 4. Return with leading slash for comparison
|
|
20
|
+
return '/' + path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isChildActive(item, currentPath) {
|
|
24
|
+
if (!item.children) return false;
|
|
25
|
+
const currentNorm = normalizePath(currentPath);
|
|
26
|
+
return item.children.some(child => {
|
|
27
|
+
const childNorm = normalizePath(child.path);
|
|
28
|
+
// Exact match or if the child itself has active children (recursion)
|
|
29
|
+
if (childNorm === currentNorm) return true;
|
|
30
|
+
return isChildActive(child, currentPath);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderNav(items) {
|
|
35
|
+
if (!items || !Array.isArray(items)) return;
|
|
36
|
+
|
|
37
|
+
items.forEach(item => {
|
|
38
|
+
const isExternal = item.external || false;
|
|
39
|
+
// An item is collapsible if it has children, unless explicitly set to false
|
|
40
|
+
const isCollapsible = item.children && item.children.length > 0 && item.collapsible !== false;
|
|
41
|
+
|
|
42
|
+
const itemNorm = normalizePath(item.path);
|
|
43
|
+
const currentNorm = normalizePath(currentPagePath);
|
|
44
|
+
|
|
45
|
+
const isActive = itemNorm === currentNorm && itemNorm !== '#';
|
|
46
|
+
const isParentOfActive = !isActive && isChildActive(item, currentPagePath);
|
|
47
|
+
|
|
48
|
+
// Auto-expand if it's a parent of the active page
|
|
49
|
+
const isOpen = isParentOfActive || (isActive && isCollapsible);
|
|
50
|
+
|
|
51
|
+
const liClasses = [];
|
|
52
|
+
if (isActive) liClasses.push('active');
|
|
53
|
+
if (isParentOfActive) liClasses.push('active-parent');
|
|
54
|
+
if (isCollapsible) liClasses.push('collapsible');
|
|
55
|
+
if (isOpen) liClasses.push('expanded');
|
|
56
|
+
|
|
57
|
+
// Construct HREF (Same as before)
|
|
58
|
+
let href = item.path || '#';
|
|
59
|
+
if (!isExternal && href !== '#' && !href.startsWith('http')) {
|
|
60
|
+
let cleanPath = href.replace(/^(\.\/|\/)+/, '');
|
|
61
|
+
href = relativePathToRoot + cleanPath;
|
|
62
|
+
if (isOfflineMode) {
|
|
63
|
+
if (href.endsWith('/')) href += 'index.html';
|
|
64
|
+
else if (!href.endsWith('.html')) href += '/index.html';
|
|
65
|
+
} else {
|
|
66
|
+
if (href.endsWith('/index.html')) href = href.slice(0, -10);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
%>
|
|
70
|
+
<li class="<%= liClasses.join(' ') %>" <% if(isCollapsible) { %> aria-expanded="<%= isOpen %>" <% } %>>
|
|
71
|
+
<a href="<%= href %>" class="<%= isActive ? 'active' : '' %>" <%= isExternal ? 'target="_blank" rel="noopener"' : '' %>>
|
|
72
|
+
<% if (item.icon) { %> <%- renderIcon(item.icon) %> <% } %>
|
|
73
|
+
<span class="nav-item-title"><%= item.title %></span>
|
|
74
|
+
|
|
75
|
+
<% if (isCollapsible) { %>
|
|
76
|
+
<span class="collapse-icon-wrapper">
|
|
77
|
+
<%- renderIcon('chevron-right', { class: 'collapse-icon' }) %>
|
|
78
|
+
</span>
|
|
79
|
+
<% } %>
|
|
80
|
+
|
|
81
|
+
<% if (isExternal) { %> <%- renderIcon('external-link', { class: 'nav-external-icon' }) %> <% } %>
|
|
82
|
+
</a>
|
|
83
|
+
|
|
84
|
+
<% if (item.children) { %>
|
|
85
|
+
<ul class="submenu">
|
|
86
|
+
<% renderNav(item.children); %>
|
|
87
|
+
</ul>
|
|
88
|
+
<% } %>
|
|
89
|
+
</li>
|
|
90
|
+
<% }); } renderNav(navItems); %>
|
|
91
|
+
</ul>
|
|
92
|
+
</nav>
|