@grafana/plugin-docs-cli 0.0.0 → 0.0.10
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/CHANGELOG.md +19 -0
- package/LICENSE +201 -0
- package/README.md +14 -0
- package/dist/bin/run.js +78 -0
- package/dist/commands/build.command.js +38 -0
- package/dist/commands/serve.command.js +30 -0
- package/dist/commands/validate.command.js +18 -0
- package/dist/scanner.js +160 -0
- package/dist/server/server.js +147 -0
- package/dist/server/styles/docs.css +430 -0
- package/dist/server/views/docs-layout.ejs +86 -0
- package/dist/server/views/partials/navigation-item.ejs +27 -0
- package/dist/server/views/partials/navigation.ejs +5 -0
- package/dist/server/views/partials/toc.ejs +13 -0
- package/dist/utils/utils.plugin.js +30 -0
- package/dist/validation/engine.js +11 -0
- package/dist/validation/format.js +43 -0
- package/dist/validation/rules/assets.js +167 -0
- package/dist/validation/rules/cross-file.js +129 -0
- package/dist/validation/rules/filesystem.js +113 -0
- package/dist/validation/rules/frontmatter.js +206 -0
- package/dist/validation/rules/index.js +17 -0
- package/dist/validation/rules/manifest.js +105 -0
- package/dist/validation/rules/markdown.js +195 -0
- package/dist/validation/rules/utils.js +18 -0
- package/dist/validation/types.js +45 -0
- package/package.json +68 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { watch } from 'chokidar';
|
|
5
|
+
import createDebug from 'debug';
|
|
6
|
+
import { parseMarkdown } from '@grafana/plugin-docs-parser';
|
|
7
|
+
import { toHtml } from 'hast-util-to-html';
|
|
8
|
+
import { scanDocsFolder } from '../scanner.js';
|
|
9
|
+
import { validate } from '../validation/engine.js';
|
|
10
|
+
import { formatResult } from '../validation/format.js';
|
|
11
|
+
import { allRules } from '../validation/rules/index.js';
|
|
12
|
+
|
|
13
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname$1 = dirname(__filename$1);
|
|
15
|
+
const debug = createDebug("plugin-docs-cli:server");
|
|
16
|
+
async function startServer(options) {
|
|
17
|
+
const { docsPath, port = 3001, liveReload = false } = options;
|
|
18
|
+
debug("Starting server with options: docsPath=%s, port=%d, liveReload=%s", docsPath, port, liveReload);
|
|
19
|
+
const app = express();
|
|
20
|
+
let lastModified = Date.now();
|
|
21
|
+
app.set("view engine", "ejs");
|
|
22
|
+
app.set("views", join(__dirname$1, "views"));
|
|
23
|
+
const runValidation = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await validate({ docsPath, strict: false }, allRules);
|
|
26
|
+
if (result.diagnostics.length > 0) {
|
|
27
|
+
console.log(formatResult(result));
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("Validation failed:", error instanceof Error ? error.message : error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
debug("Scanning docs folder: %s", docsPath);
|
|
34
|
+
const scanned = await scanDocsFolder(docsPath);
|
|
35
|
+
let manifest = scanned.manifest;
|
|
36
|
+
let files = scanned.files;
|
|
37
|
+
debug("Manifest generated with %d pages, %d files loaded", manifest.pages.length, Object.keys(files).length);
|
|
38
|
+
await runValidation();
|
|
39
|
+
const watcher = watch(join(docsPath, "**/*.md"), {
|
|
40
|
+
ignoreInitial: true
|
|
41
|
+
});
|
|
42
|
+
debug("File watcher initialized for %s", docsPath);
|
|
43
|
+
const rescan = async (event, path) => {
|
|
44
|
+
debug("File %s: %s", event, path);
|
|
45
|
+
lastModified = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
const rescanned = await scanDocsFolder(docsPath);
|
|
48
|
+
manifest = rescanned.manifest;
|
|
49
|
+
files = rescanned.files;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Error re-scanning docs folder:", error);
|
|
52
|
+
}
|
|
53
|
+
await runValidation();
|
|
54
|
+
};
|
|
55
|
+
watcher.on("change", (p) => rescan("changed", p));
|
|
56
|
+
watcher.on("add", (p) => rescan("added", p));
|
|
57
|
+
watcher.on("unlink", (p) => rescan("removed", p));
|
|
58
|
+
const docsStatic = express.static(docsPath, { index: false, redirect: false, dotfiles: "ignore", extensions: [] });
|
|
59
|
+
app.use((req, res, next) => {
|
|
60
|
+
if (req.path.endsWith(".md")) {
|
|
61
|
+
return next();
|
|
62
|
+
}
|
|
63
|
+
docsStatic(req, res, next);
|
|
64
|
+
});
|
|
65
|
+
app.use("/styles", express.static(join(__dirname$1, "styles")));
|
|
66
|
+
if (liveReload) {
|
|
67
|
+
app.get("/__reload__", (req, res) => {
|
|
68
|
+
const clientTime = parseInt(req.query.t, 10) || 0;
|
|
69
|
+
if (lastModified > clientTime) {
|
|
70
|
+
res.status(205).send();
|
|
71
|
+
} else {
|
|
72
|
+
res.status(204).send();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function findPageBySlug(slug, pages) {
|
|
77
|
+
for (const page of pages) {
|
|
78
|
+
if (page.slug === slug) {
|
|
79
|
+
return page;
|
|
80
|
+
}
|
|
81
|
+
if (page.children) {
|
|
82
|
+
const found = findPageBySlug(slug, page.children);
|
|
83
|
+
if (found) {
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
app.get("/{*splat}", async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const slug = req.path === "/" ? manifest.pages[0]?.slug || "" : req.path.replace(/^\/|\/$/g, "");
|
|
93
|
+
if (!slug) {
|
|
94
|
+
debug("No pages found in manifest");
|
|
95
|
+
res.status(404).send("No pages found in manifest");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
debug("Request for slug: %s", slug);
|
|
99
|
+
const page = findPageBySlug(slug, manifest.pages);
|
|
100
|
+
if (!page) {
|
|
101
|
+
debug("Page not found for slug: %s", slug);
|
|
102
|
+
res.status(404).send("Page not found");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const fileContent = files[page.file];
|
|
106
|
+
if (!fileContent) {
|
|
107
|
+
debug("File content not found in memory for: %s", page.file);
|
|
108
|
+
res.status(404).send("File content not found");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const parsed = parseMarkdown(fileContent);
|
|
112
|
+
const title = page.title || slug;
|
|
113
|
+
res.render("docs-layout", {
|
|
114
|
+
title,
|
|
115
|
+
content: toHtml(parsed.hast),
|
|
116
|
+
manifest,
|
|
117
|
+
currentPath: slug,
|
|
118
|
+
headings: parsed.headings,
|
|
119
|
+
liveReload
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("Error serving page:", error);
|
|
123
|
+
res.status(500).send("Internal server error");
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const server = app.listen(port, () => {
|
|
127
|
+
const addr = server.address();
|
|
128
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
129
|
+
console.log(`
|
|
130
|
+
\u{1F4C4} Plugin Documentation Server`);
|
|
131
|
+
console.log(`\u2713 Serving: ${docsPath}`);
|
|
132
|
+
console.log(`\u2713 URL: http://localhost:${actualPort}`);
|
|
133
|
+
console.log(`\u2713 Live reload: ${liveReload ? "enabled" : "disabled"}`);
|
|
134
|
+
console.log(`
|
|
135
|
+
\u{1F50D} Watching for changes...
|
|
136
|
+
`);
|
|
137
|
+
});
|
|
138
|
+
const close = async () => {
|
|
139
|
+
await watcher.close();
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
return { app, close };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { startServer };
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Grafana-themed CSS for Plugin Documentation Renderer
|
|
3
|
+
* Based on Grafana design tokens and marketing website styling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* ==================== CSS Variables (Design Tokens) ==================== */
|
|
7
|
+
:root {
|
|
8
|
+
/* Colors - Grafana brand */
|
|
9
|
+
--color-primary: #ff8833;
|
|
10
|
+
--color-primary-hover: #ff6a00;
|
|
11
|
+
|
|
12
|
+
/* Grays */
|
|
13
|
+
--color-gray-50: #f7f8fa;
|
|
14
|
+
--color-gray-100: #eaedf0;
|
|
15
|
+
--color-gray-200: #d8d8df;
|
|
16
|
+
--color-gray-300: #c7c7d1;
|
|
17
|
+
--color-gray-400: #9e9ea8;
|
|
18
|
+
--color-gray-500: #6e6e78;
|
|
19
|
+
--color-gray-600: #52525c;
|
|
20
|
+
--color-gray-700: #3d3d47;
|
|
21
|
+
--color-gray-800: #24242b;
|
|
22
|
+
--color-gray-900: #18181d;
|
|
23
|
+
|
|
24
|
+
/* Blues */
|
|
25
|
+
--color-blue-500: #2970ff;
|
|
26
|
+
--color-blue-600: #1b55f5;
|
|
27
|
+
--color-blue-100: #d9e8ff;
|
|
28
|
+
|
|
29
|
+
/* Greens */
|
|
30
|
+
--color-green-500: #17b26a;
|
|
31
|
+
--color-green-100: #d3f8e0;
|
|
32
|
+
|
|
33
|
+
/* Typography */
|
|
34
|
+
--font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
35
|
+
--font-family-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
|
36
|
+
|
|
37
|
+
/* Font sizes */
|
|
38
|
+
--text-xs: 0.75rem; /* 12px */
|
|
39
|
+
--text-sm: 0.875rem; /* 14px */
|
|
40
|
+
--text-base: 1rem; /* 16px */
|
|
41
|
+
--text-lg: 1.125rem; /* 18px */
|
|
42
|
+
--text-xl: 1.25rem; /* 20px */
|
|
43
|
+
--text-2xl: 1.5rem; /* 24px */
|
|
44
|
+
--text-3xl: 1.875rem; /* 30px */
|
|
45
|
+
|
|
46
|
+
/* Spacing */
|
|
47
|
+
--space-1: 0.25rem; /* 4px */
|
|
48
|
+
--space-2: 0.5rem; /* 8px */
|
|
49
|
+
--space-3: 0.75rem; /* 12px */
|
|
50
|
+
--space-4: 1rem; /* 16px */
|
|
51
|
+
--space-6: 1.5rem; /* 24px */
|
|
52
|
+
--space-8: 2rem; /* 32px */
|
|
53
|
+
--space-12: 3rem; /* 48px */
|
|
54
|
+
|
|
55
|
+
/* Borders */
|
|
56
|
+
--border-radius: 0.375rem;
|
|
57
|
+
--border-color: var(--color-gray-200);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ==================== Base Styles ==================== */
|
|
61
|
+
* {
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
body {
|
|
66
|
+
font-family: var(--font-family-sans);
|
|
67
|
+
font-size: var(--text-base);
|
|
68
|
+
line-height: 1.6;
|
|
69
|
+
color: var(--color-gray-900);
|
|
70
|
+
margin: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
background-color: var(--color-gray-50);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ==================== Header ==================== */
|
|
76
|
+
.docs-header {
|
|
77
|
+
background-color: var(--color-gray-900);
|
|
78
|
+
color: white;
|
|
79
|
+
padding: var(--space-4);
|
|
80
|
+
border-bottom: 2px solid var(--color-primary);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.docs-header-content {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--space-4);
|
|
87
|
+
max-width: 1400px;
|
|
88
|
+
margin: 0 auto;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.docs-header-title {
|
|
92
|
+
font-size: var(--text-xl);
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
margin: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.docs-header-badge {
|
|
98
|
+
font-size: var(--text-sm);
|
|
99
|
+
color: var(--color-gray-400);
|
|
100
|
+
background-color: var(--color-gray-800);
|
|
101
|
+
padding: var(--space-1) var(--space-3);
|
|
102
|
+
border-radius: var(--border-radius);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ==================== Three-Column Layout ==================== */
|
|
106
|
+
.docs-container {
|
|
107
|
+
display: grid;
|
|
108
|
+
grid-template-columns: 250px 1fr 200px;
|
|
109
|
+
gap: var(--space-8);
|
|
110
|
+
max-width: 1400px;
|
|
111
|
+
margin: 0 auto;
|
|
112
|
+
padding: var(--space-8);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Responsive: collapse sidebars on mobile */
|
|
116
|
+
@media (max-width: 768px) {
|
|
117
|
+
.docs-container {
|
|
118
|
+
grid-template-columns: 1fr;
|
|
119
|
+
padding: var(--space-4);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.docs-nav,
|
|
123
|
+
.docs-toc {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ==================== Left Sidebar: Navigation ==================== */
|
|
129
|
+
.docs-nav {
|
|
130
|
+
position: sticky;
|
|
131
|
+
top: var(--space-4);
|
|
132
|
+
height: fit-content;
|
|
133
|
+
max-height: calc(100vh - var(--space-8));
|
|
134
|
+
overflow-y: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.docs-nav-title {
|
|
138
|
+
font-size: var(--text-lg);
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
margin: 0 0 var(--space-4) 0;
|
|
141
|
+
color: var(--color-gray-900);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.docs-nav-list {
|
|
145
|
+
list-style: none;
|
|
146
|
+
padding: 0;
|
|
147
|
+
margin: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.docs-nav-item {
|
|
151
|
+
margin-bottom: var(--space-1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* collapsible items */
|
|
155
|
+
.docs-nav-item.collapsible {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-wrap: wrap;
|
|
158
|
+
align-items: center;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* toggle button for collapsible sections */
|
|
162
|
+
.docs-nav-toggle {
|
|
163
|
+
background: none;
|
|
164
|
+
border: none;
|
|
165
|
+
padding: var(--space-2);
|
|
166
|
+
margin: 0;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
color: var(--color-gray-600);
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
transition: transform 0.2s ease;
|
|
173
|
+
flex-shrink: 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.docs-nav-toggle:hover {
|
|
177
|
+
color: var(--color-gray-900);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.docs-nav-toggle svg {
|
|
181
|
+
transition: transform 0.2s ease;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* rotate chevron when expanded */
|
|
185
|
+
.docs-nav-item.expanded > .docs-nav-toggle svg {
|
|
186
|
+
transform: rotate(90deg);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.docs-nav-link {
|
|
190
|
+
display: block;
|
|
191
|
+
padding: var(--space-2) var(--space-3);
|
|
192
|
+
color: var(--color-gray-700);
|
|
193
|
+
text-decoration: none;
|
|
194
|
+
border-radius: var(--border-radius);
|
|
195
|
+
font-size: var(--text-sm);
|
|
196
|
+
transition: all 0.15s ease;
|
|
197
|
+
flex: 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.docs-nav-link:hover {
|
|
201
|
+
background-color: var(--color-gray-100);
|
|
202
|
+
color: var(--color-gray-900);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.docs-nav-link.active {
|
|
206
|
+
background-color: var(--color-blue-100);
|
|
207
|
+
color: var(--color-blue-600);
|
|
208
|
+
font-weight: 600;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* non-clickable labels for sections without files */
|
|
212
|
+
.docs-nav-label {
|
|
213
|
+
display: block;
|
|
214
|
+
padding: var(--space-2) var(--space-3);
|
|
215
|
+
color: var(--color-gray-900);
|
|
216
|
+
font-size: var(--text-sm);
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
flex: 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* nested navigation items */
|
|
222
|
+
.docs-nav-list .docs-nav-list {
|
|
223
|
+
margin-left: var(--space-4);
|
|
224
|
+
margin-top: var(--space-1);
|
|
225
|
+
width: 100%;
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* show nested list when parent is expanded */
|
|
230
|
+
.docs-nav-item.expanded > .docs-nav-list {
|
|
231
|
+
display: block;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ==================== Center: Main Content ==================== */
|
|
235
|
+
.docs-content {
|
|
236
|
+
background-color: white;
|
|
237
|
+
padding: var(--space-8);
|
|
238
|
+
border-radius: var(--border-radius);
|
|
239
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
240
|
+
min-height: 400px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Typography for content */
|
|
244
|
+
.docs-content h1 {
|
|
245
|
+
font-size: var(--text-3xl);
|
|
246
|
+
font-weight: 700;
|
|
247
|
+
color: var(--color-gray-900);
|
|
248
|
+
margin: 0 0 var(--space-6) 0;
|
|
249
|
+
line-height: 1.2;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.docs-content h2 {
|
|
253
|
+
font-size: var(--text-2xl);
|
|
254
|
+
font-weight: 600;
|
|
255
|
+
color: var(--color-gray-900);
|
|
256
|
+
margin: var(--space-8) 0 var(--space-4) 0;
|
|
257
|
+
padding-top: var(--space-4);
|
|
258
|
+
border-top: 1px solid var(--color-gray-200);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.docs-content h2:first-child {
|
|
262
|
+
margin-top: 0;
|
|
263
|
+
padding-top: 0;
|
|
264
|
+
border-top: none;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.docs-content h3 {
|
|
268
|
+
font-size: var(--text-xl);
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
color: var(--color-gray-900);
|
|
271
|
+
margin: var(--space-6) 0 var(--space-3) 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.docs-content p {
|
|
275
|
+
margin: 0 0 var(--space-4) 0;
|
|
276
|
+
line-height: 1.7;
|
|
277
|
+
color: var(--color-gray-700);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.docs-content a {
|
|
281
|
+
color: var(--color-blue-500);
|
|
282
|
+
text-decoration: none;
|
|
283
|
+
border-bottom: 1px solid transparent;
|
|
284
|
+
transition: border-color 0.15s ease;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.docs-content a:hover {
|
|
288
|
+
border-bottom-color: var(--color-blue-500);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.docs-content ul,
|
|
292
|
+
.docs-content ol {
|
|
293
|
+
margin: 0 0 var(--space-4) 0;
|
|
294
|
+
padding-left: var(--space-6);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.docs-content li {
|
|
298
|
+
margin-bottom: var(--space-2);
|
|
299
|
+
line-height: 1.7;
|
|
300
|
+
color: var(--color-gray-700);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.docs-content code {
|
|
304
|
+
font-family: var(--font-family-mono);
|
|
305
|
+
font-size: 0.9em;
|
|
306
|
+
background-color: var(--color-gray-100);
|
|
307
|
+
padding: 0.125rem 0.375rem;
|
|
308
|
+
border-radius: 0.25rem;
|
|
309
|
+
color: var(--color-gray-900);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.docs-content pre {
|
|
313
|
+
background-color: var(--color-gray-900);
|
|
314
|
+
color: var(--color-gray-100);
|
|
315
|
+
padding: var(--space-4);
|
|
316
|
+
border-radius: var(--border-radius);
|
|
317
|
+
overflow-x: auto;
|
|
318
|
+
margin: 0 0 var(--space-4) 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.docs-content pre code {
|
|
322
|
+
background-color: transparent;
|
|
323
|
+
padding: 0;
|
|
324
|
+
color: inherit;
|
|
325
|
+
font-size: var(--text-sm);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.docs-content blockquote {
|
|
329
|
+
border-left: 4px solid var(--color-blue-500);
|
|
330
|
+
padding-left: var(--space-4);
|
|
331
|
+
margin: var(--space-4) 0;
|
|
332
|
+
color: var(--color-gray-600);
|
|
333
|
+
font-style: italic;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.docs-content table {
|
|
337
|
+
width: 100%;
|
|
338
|
+
border-collapse: collapse;
|
|
339
|
+
margin: var(--space-4) 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.docs-content th,
|
|
343
|
+
.docs-content td {
|
|
344
|
+
padding: var(--space-3);
|
|
345
|
+
text-align: left;
|
|
346
|
+
border-bottom: 1px solid var(--color-gray-200);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.docs-content th {
|
|
350
|
+
font-weight: 600;
|
|
351
|
+
background-color: var(--color-gray-50);
|
|
352
|
+
color: var(--color-gray-900);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.docs-content img {
|
|
356
|
+
max-width: 100%;
|
|
357
|
+
height: auto;
|
|
358
|
+
border-radius: var(--border-radius);
|
|
359
|
+
margin: var(--space-4) 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* ==================== Right Sidebar: Table of Contents ==================== */
|
|
363
|
+
.docs-toc {
|
|
364
|
+
position: sticky;
|
|
365
|
+
top: var(--space-4);
|
|
366
|
+
height: fit-content;
|
|
367
|
+
max-height: calc(100vh - var(--space-8));
|
|
368
|
+
overflow-y: auto;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.docs-toc-title {
|
|
372
|
+
font-size: var(--text-sm);
|
|
373
|
+
font-weight: 600;
|
|
374
|
+
color: var(--color-gray-700);
|
|
375
|
+
margin: 0 0 var(--space-3) 0;
|
|
376
|
+
text-transform: uppercase;
|
|
377
|
+
letter-spacing: 0.05em;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.docs-toc-list {
|
|
381
|
+
list-style: none;
|
|
382
|
+
padding: 0;
|
|
383
|
+
margin: 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.docs-toc-item {
|
|
387
|
+
margin-bottom: var(--space-2);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.docs-toc-link {
|
|
391
|
+
display: block;
|
|
392
|
+
color: var(--color-gray-600);
|
|
393
|
+
text-decoration: none;
|
|
394
|
+
font-size: var(--text-sm);
|
|
395
|
+
line-height: 1.5;
|
|
396
|
+
transition: color 0.15s ease;
|
|
397
|
+
padding: var(--space-1) 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.docs-toc-link:hover {
|
|
401
|
+
color: var(--color-blue-500);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Nested TOC items (h3) */
|
|
405
|
+
.docs-toc-list .docs-toc-list {
|
|
406
|
+
margin-left: var(--space-3);
|
|
407
|
+
margin-top: var(--space-1);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ==================== Scrollbar Styling ==================== */
|
|
411
|
+
.docs-nav::-webkit-scrollbar,
|
|
412
|
+
.docs-toc::-webkit-scrollbar {
|
|
413
|
+
width: 6px;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.docs-nav::-webkit-scrollbar-track,
|
|
417
|
+
.docs-toc::-webkit-scrollbar-track {
|
|
418
|
+
background: transparent;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.docs-nav::-webkit-scrollbar-thumb,
|
|
422
|
+
.docs-toc::-webkit-scrollbar-thumb {
|
|
423
|
+
background: var(--color-gray-300);
|
|
424
|
+
border-radius: 3px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.docs-nav::-webkit-scrollbar-thumb:hover,
|
|
428
|
+
.docs-toc::-webkit-scrollbar-thumb:hover {
|
|
429
|
+
background: var(--color-gray-400);
|
|
430
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= title %> - <%= manifest.title %></title>
|
|
7
|
+
<link rel="stylesheet" href="/styles/docs.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!-- Header -->
|
|
11
|
+
<header class="docs-header">
|
|
12
|
+
<div class="docs-header-content">
|
|
13
|
+
<p class="docs-header-title">📦 <%= manifest.title %></p>
|
|
14
|
+
<span class="docs-header-badge">Local Preview</span>
|
|
15
|
+
</div>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<!-- Three-column layout: nav | content | toc -->
|
|
19
|
+
<div class="docs-container">
|
|
20
|
+
<!-- Left: Docs navigation tree -->
|
|
21
|
+
<nav class="docs-nav">
|
|
22
|
+
<h2 class="docs-nav-title">Documentation</h2>
|
|
23
|
+
<%- include('partials/navigation', { pages: manifest.pages, currentPath }) %>
|
|
24
|
+
</nav>
|
|
25
|
+
|
|
26
|
+
<!-- Center: Main content -->
|
|
27
|
+
<main class="docs-content">
|
|
28
|
+
<h1 class="docs-page-title"><%= title %></h1>
|
|
29
|
+
<%- content %>
|
|
30
|
+
</main>
|
|
31
|
+
|
|
32
|
+
<!-- Right: Table of contents -->
|
|
33
|
+
<aside class="docs-toc">
|
|
34
|
+
<h3 class="docs-toc-title">On this page</h3>
|
|
35
|
+
<%- include('partials/toc', { headings }) %>
|
|
36
|
+
</aside>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
// collapsible navigation
|
|
41
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
42
|
+
const toggleButtons = document.querySelectorAll('.docs-nav-toggle');
|
|
43
|
+
|
|
44
|
+
toggleButtons.forEach(button => {
|
|
45
|
+
button.addEventListener('click', (e) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
const navItem = button.closest('.docs-nav-item');
|
|
48
|
+
navItem.classList.toggle('expanded');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// expand active page's own section and all ancestor sections
|
|
53
|
+
const activeLink = document.querySelector('.docs-nav-link.active');
|
|
54
|
+
if (activeLink) {
|
|
55
|
+
const ownItem = activeLink.closest('.docs-nav-item');
|
|
56
|
+
if (ownItem) ownItem.classList.add('expanded');
|
|
57
|
+
let parent = activeLink.closest('.docs-nav-list');
|
|
58
|
+
while (parent) {
|
|
59
|
+
const parentItem = parent.closest('.docs-nav-item');
|
|
60
|
+
if (parentItem) {
|
|
61
|
+
parentItem.classList.add('expanded');
|
|
62
|
+
}
|
|
63
|
+
parent = parentItem?.parentElement.closest('.docs-nav-list');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<% if (liveReload) { %>
|
|
70
|
+
<script>
|
|
71
|
+
let lastCheck = Date.now();
|
|
72
|
+
setInterval(async () => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch('/__reload__?t=' + lastCheck);
|
|
75
|
+
if (res.status === 205) {
|
|
76
|
+
location.reload();
|
|
77
|
+
}
|
|
78
|
+
lastCheck = Date.now();
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// ignore fetch errors
|
|
81
|
+
}
|
|
82
|
+
}, 1000);
|
|
83
|
+
</script>
|
|
84
|
+
<% } %>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const isActive = page.slug === currentPath;
|
|
3
|
+
const hasChildren = page.children && page.children.length > 0;
|
|
4
|
+
const hasFile = page.file && page.file.length > 0;
|
|
5
|
+
const href = '/' + page.slug;
|
|
6
|
+
-%>
|
|
7
|
+
<li class="docs-nav-item<%= hasChildren ? ' collapsible' : '' %>">
|
|
8
|
+
<% if (hasChildren) { %>
|
|
9
|
+
<button class="docs-nav-toggle" aria-label="Toggle section">
|
|
10
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
11
|
+
<path d="M4.5 3L7.5 6L4.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
12
|
+
</svg>
|
|
13
|
+
</button>
|
|
14
|
+
<% } %>
|
|
15
|
+
<% if (hasFile) { %>
|
|
16
|
+
<a href="<%= href %>" class="docs-nav-link<%= isActive ? ' active' : '' %>"><%= page.title %></a>
|
|
17
|
+
<% } else { %>
|
|
18
|
+
<span class="docs-nav-label"><%= page.title %></span>
|
|
19
|
+
<% } %>
|
|
20
|
+
<% if (hasChildren) { %>
|
|
21
|
+
<ul class="docs-nav-list">
|
|
22
|
+
<% page.children.forEach(child => { %>
|
|
23
|
+
<%- include('navigation-item', { page: child, currentPath }) %>
|
|
24
|
+
<% }) %>
|
|
25
|
+
</ul>
|
|
26
|
+
<% } %>
|
|
27
|
+
</li>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% if (!headings || headings.length === 0) { %>
|
|
2
|
+
<p style="color: var(--color-gray-500); font-size: var(--text-sm);">No headings found</p>
|
|
3
|
+
<% } else { %>
|
|
4
|
+
<ul class="docs-toc-list">
|
|
5
|
+
<% headings.forEach(heading => { %>
|
|
6
|
+
<li class="docs-toc-item" <%= heading.level === 3 ? 'style="margin-left: var(--space-3);"' : '' %>>
|
|
7
|
+
<a href="#<%= heading.id %>" class="docs-toc-link">
|
|
8
|
+
<%= heading.text %>
|
|
9
|
+
</a>
|
|
10
|
+
</li>
|
|
11
|
+
<% }); %>
|
|
12
|
+
</ul>
|
|
13
|
+
<% } %>
|