@writedocs/generator 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/writedocs.js +12 -5
- package/package.json +90 -87
- package/src/cli/convert.js +44 -20
- package/src/cli/generate-api-pages.js +24 -2
- package/src/layout/BaseLayout.astro +13 -15
- package/src/layout/styles/topbar.css +17 -34
- package/src/lib/config-schema.js +1 -1
- package/src/lib/config-schema.ts +2 -2
- package/src/lib/content-check.js +41 -0
- package/src/lib/json-schema-descriptions.js +204 -0
- package/src/lib/json-schema.js +91 -0
- package/src/lib/link-check.js +23 -11
- package/src/lib/mintlify-convert.js +2 -2
- package/src/lib/writedocs-legacy-convert.js +496 -0
- package/writedocs.schema.json +2291 -0
package/bin/writedocs.js
CHANGED
|
@@ -238,20 +238,27 @@ program
|
|
|
238
238
|
program
|
|
239
239
|
.command('convert')
|
|
240
240
|
.description("Convert another docs tool's config into writedocs.json")
|
|
241
|
-
.argument('[dir]', 'project directory (contains docs.json)', '.')
|
|
241
|
+
.argument('[dir]', 'project directory (contains docs.json or config.json)', '.')
|
|
242
242
|
.option('--mintlify', "convert a Mintlify project's docs.json")
|
|
243
243
|
.option('--docs.json', 'same as --mintlify')
|
|
244
|
+
.option('--writedocs', "convert a project from the previous writedocs (its config.json)")
|
|
245
|
+
.option('--config.json', 'same as --writedocs')
|
|
244
246
|
.option('--force', 'overwrite an existing writedocs.json')
|
|
245
247
|
.option('--dry-run', 'print the converted writedocs.json instead of writing it')
|
|
246
248
|
.action(async (dir, options) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
log.error(
|
|
249
|
+
const mintlify = options.mintlify || options.docsJson;
|
|
250
|
+
const legacy = options.writedocs || options.configJson;
|
|
251
|
+
if (mintlify === legacy) {
|
|
252
|
+
log.error(
|
|
253
|
+
mintlify
|
|
254
|
+
? 'Pick one: --mintlify or --writedocs.'
|
|
255
|
+
: 'Say what to convert from: writedocs convert --mintlify [dir], or --writedocs for the previous writedocs.'
|
|
256
|
+
);
|
|
251
257
|
throw new CliExit(1);
|
|
252
258
|
}
|
|
253
259
|
const { runConvert } = await import('../src/cli/convert.js');
|
|
254
260
|
await runConvert({
|
|
261
|
+
source: legacy ? 'writedocs' : 'mintlify',
|
|
255
262
|
contentDir: path.resolve(process.cwd(), dir),
|
|
256
263
|
force: Boolean(options.force),
|
|
257
264
|
dryRun: Boolean(options.dryRun),
|
package/package.json
CHANGED
|
@@ -1,88 +1,91 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@writedocs/generator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"writedocs": "./bin/writedocs.js"
|
|
8
|
-
},
|
|
9
|
-
"exports": {
|
|
10
|
-
"./components": "./src/components/index.ts",
|
|
11
|
-
"./config-schema": "./src/lib/config-schema.js"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"build
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"url": "https://github.com/writedocs/writedocs
|
|
40
|
-
},
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"@
|
|
51
|
-
"@astrojs/
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
55
|
-
"@iconify-json/
|
|
56
|
-
"@iconify-json/
|
|
57
|
-
"@iconify-json/
|
|
58
|
-
"@iconify-json/
|
|
59
|
-
"@iconify-json/
|
|
60
|
-
"@iconify-json/
|
|
61
|
-
"@iconify-json/
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
64
|
-
"@
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
},
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@writedocs/generator",
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"writedocs": "./bin/writedocs.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./components": "./src/components/index.ts",
|
|
11
|
+
"./config-schema": "./src/lib/config-schema.js",
|
|
12
|
+
"./writedocs.schema.json": "./writedocs.schema.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"astro.config.mjs",
|
|
18
|
+
"writedocs.schema.json"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "node bin/writedocs.js dev",
|
|
22
|
+
"build": "node bin/writedocs.js build",
|
|
23
|
+
"init": "node bin/writedocs.js init",
|
|
24
|
+
"build:schema": "esbuild src/lib/config-schema.ts --format=esm --target=node22 --outfile=src/lib/config-schema.js",
|
|
25
|
+
"prepare": "npm run build:schema && npm run build:json-schema",
|
|
26
|
+
"test": "node --test \"scripts/*.test.js\"",
|
|
27
|
+
"build:json-schema": "node scripts/build-json-schema.mjs"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"docs",
|
|
31
|
+
"documentation",
|
|
32
|
+
"static-site-generator",
|
|
33
|
+
"mdx",
|
|
34
|
+
"astro"
|
|
35
|
+
],
|
|
36
|
+
"homepage": "https://github.com/writedocs/writedocs#readme",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/writedocs/writedocs.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/writedocs/writedocs/issues"
|
|
43
|
+
},
|
|
44
|
+
"author": "Gabriel Raeder",
|
|
45
|
+
"license": "ISC",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@apidevtools/swagger-parser": "^12.1.0",
|
|
51
|
+
"@astrojs/markdown-remark": "7.2.4",
|
|
52
|
+
"@astrojs/mdx": "^7.0.5",
|
|
53
|
+
"@astrojs/react": "^6.0.2",
|
|
54
|
+
"@astrojs/sitemap": "^3.7.3",
|
|
55
|
+
"@iconify-json/bi": "^1.2.7",
|
|
56
|
+
"@iconify-json/fa6-brands": "^1.2.6",
|
|
57
|
+
"@iconify-json/fa6-solid": "^1.2.4",
|
|
58
|
+
"@iconify-json/heroicons": "^1.2.3",
|
|
59
|
+
"@iconify-json/ion": "^1.2.7",
|
|
60
|
+
"@iconify-json/lucide": "^1.2.116",
|
|
61
|
+
"@iconify-json/mdi": "^1.2.3",
|
|
62
|
+
"@iconify-json/ri": "^1.2.10",
|
|
63
|
+
"@iconify-json/simple-icons": "^1.2.89",
|
|
64
|
+
"@iconify-json/tabler": "^1.2.35",
|
|
65
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
66
|
+
"@shikijs/transformers": "^4.3.1",
|
|
67
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
68
|
+
"astro": "7.2.9",
|
|
69
|
+
"astro-icon": "^1.1.5",
|
|
70
|
+
"commander": "^15.0.0",
|
|
71
|
+
"dotenv": "^17.4.2",
|
|
72
|
+
"gray-matter": "^4.0.3",
|
|
73
|
+
"jsonc-parser": "3.3.1",
|
|
74
|
+
"katex": "^0.16.47",
|
|
75
|
+
"mermaid": "^11.16.0",
|
|
76
|
+
"pagefind": "^1.5.2",
|
|
77
|
+
"react": "^19.2.8",
|
|
78
|
+
"react-dom": "^19.2.8",
|
|
79
|
+
"rehype-katex": "^7.0.1",
|
|
80
|
+
"remark-math": "^6.0.0",
|
|
81
|
+
"shiki": "^4.3.1",
|
|
82
|
+
"tailwindcss": "^4.3.2",
|
|
83
|
+
"zod": "^4.4.3"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=22.12.0"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"esbuild": "0.28.2"
|
|
90
|
+
}
|
|
88
91
|
}
|
package/src/cli/convert.js
CHANGED
|
@@ -1,24 +1,48 @@
|
|
|
1
|
-
// `writedocs convert --mintlify [dir]` - turns a
|
|
2
|
-
// docs.json
|
|
3
|
-
// the same
|
|
4
|
-
//
|
|
1
|
+
// `writedocs convert --mintlify [dir]` / `--writedocs [dir]` - turns a
|
|
2
|
+
// Mintlify project's docs.json, or a config.json from the previous writedocs,
|
|
3
|
+
// into writedocs.json, in the same folder, then checks the pages the same way
|
|
4
|
+
// `writedocs validate` does. The conversions themselves are
|
|
5
|
+
// lib/mintlify-convert.js and lib/writedocs-legacy-convert.js; this file is
|
|
6
|
+
// the CLI around them.
|
|
5
7
|
import fs from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { loadMintlifyConfig, convertMintlifyConfig, formatNotes } from '../lib/mintlify-convert.js';
|
|
10
|
+
import { loadLegacyConfig, convertLegacyConfig } from '../lib/writedocs-legacy-convert.js';
|
|
8
11
|
import { validateDocsConfig, formatValidationIssuesDetailed } from '../lib/config-schema.js';
|
|
9
12
|
import { checkContent, formatContentIssues } from '../lib/content-check.js';
|
|
10
13
|
import { log, step, plural, color, displayPath, CliExit } from './output.js';
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const SOURCES = {
|
|
16
|
+
mintlify: {
|
|
17
|
+
file: 'docs.json',
|
|
18
|
+
load: loadMintlifyConfig,
|
|
19
|
+
convert: async (input) => convertMintlifyConfig(input),
|
|
20
|
+
missing(contentDir) {
|
|
21
|
+
if (fs.existsSync(path.join(contentDir, 'mint.json'))) {
|
|
22
|
+
log.error("Found mint.json, Mintlify's older config format.");
|
|
23
|
+
log.detail(color.dim(`Run "npx mint upgrade" in ${contentDir} to turn it into docs.json, then run this again.`));
|
|
24
|
+
} else {
|
|
25
|
+
log.error(`No docs.json found in ${contentDir}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
domainHint: '(Mintlify sets this in its dashboard, not docs.json.)',
|
|
29
|
+
},
|
|
30
|
+
writedocs: {
|
|
31
|
+
file: 'config.json',
|
|
32
|
+
load: loadLegacyConfig,
|
|
33
|
+
convert: (input, contentDir) => convertLegacyConfig(input, contentDir),
|
|
34
|
+
missing(contentDir) {
|
|
35
|
+
log.error(`No config.json found in ${contentDir}`);
|
|
36
|
+
},
|
|
37
|
+
domainHint: '(config.json had no field for it.)',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export async function runConvert({ source = 'mintlify', contentDir, force = false, dryRun = false }) {
|
|
42
|
+
const from = SOURCES[source];
|
|
43
|
+
const inputPath = path.join(contentDir, from.file);
|
|
44
|
+
if (!fs.existsSync(inputPath)) {
|
|
45
|
+
from.missing(contentDir);
|
|
22
46
|
throw new CliExit(1);
|
|
23
47
|
}
|
|
24
48
|
const outPath = path.join(contentDir, 'writedocs.json');
|
|
@@ -28,15 +52,15 @@ export async function runConvert({ contentDir, force = false, dryRun = false })
|
|
|
28
52
|
throw new CliExit(1);
|
|
29
53
|
}
|
|
30
54
|
|
|
31
|
-
let
|
|
55
|
+
let input;
|
|
32
56
|
try {
|
|
33
|
-
|
|
57
|
+
input = from.load(inputPath);
|
|
34
58
|
} catch (err) {
|
|
35
|
-
log.error(`Couldn't read ${displayPath(
|
|
59
|
+
log.error(`Couldn't read ${displayPath(inputPath)}: ${err.message}`);
|
|
36
60
|
throw new CliExit(1);
|
|
37
61
|
}
|
|
38
62
|
|
|
39
|
-
const { config, notes } =
|
|
63
|
+
const { config, notes } = await from.convert(input, contentDir);
|
|
40
64
|
const text = `${JSON.stringify(config, null, 2)}\n`;
|
|
41
65
|
|
|
42
66
|
// The result must pass writedocs' own schema - if it doesn't, that's a
|
|
@@ -54,7 +78,7 @@ export async function runConvert({ contentDir, force = false, dryRun = false })
|
|
|
54
78
|
log.line(text);
|
|
55
79
|
} else {
|
|
56
80
|
fs.writeFileSync(outPath, text);
|
|
57
|
-
log.success(`Converted ${
|
|
81
|
+
log.success(`Converted ${from.file} to ${color.bold(displayPath(outPath))}`);
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
if (notes.length) {
|
|
@@ -87,7 +111,7 @@ export async function runConvert({ contentDir, force = false, dryRun = false })
|
|
|
87
111
|
else log.success(summary);
|
|
88
112
|
if (!config.domain) {
|
|
89
113
|
log.info(
|
|
90
|
-
|
|
114
|
+
`Next: set "domain" in writedocs.json to your site's URL - it turns on sitemap.xml and absolute links for social previews. ${from.domainHint}`
|
|
91
115
|
);
|
|
92
116
|
}
|
|
93
117
|
}
|
|
@@ -168,6 +168,28 @@ function buildOperations(spec) {
|
|
|
168
168
|
return operations;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/** An operation as JSON. A recursive schema - a tree whose nodes contain
|
|
172
|
+
* nodes - dereferences into a cycle (SwaggerParser.dereference() resolves
|
|
173
|
+
* the $ref to the very same object), which JSON can't hold and the API
|
|
174
|
+
* pages couldn't render anyway. Where a schema would contain itself, a
|
|
175
|
+
* short stand-in takes its place: its type and title, marked
|
|
176
|
+
* x-circular. Shared, non-recursive schemas are written in full each time,
|
|
177
|
+
* as before. */
|
|
178
|
+
function operationJson(operation) {
|
|
179
|
+
const ancestors = [];
|
|
180
|
+
const cut = (node) => {
|
|
181
|
+
if (node === null || typeof node !== 'object') return node;
|
|
182
|
+
if (ancestors.includes(node)) {
|
|
183
|
+
return { ...(node.type ? { type: node.type } : {}), ...(node.title ? { title: node.title } : {}), 'x-circular': true };
|
|
184
|
+
}
|
|
185
|
+
ancestors.push(node);
|
|
186
|
+
const out = Array.isArray(node) ? node.map(cut) : Object.fromEntries(Object.entries(node).map(([k, v]) => [k, cut(v)]));
|
|
187
|
+
ancestors.pop();
|
|
188
|
+
return out;
|
|
189
|
+
};
|
|
190
|
+
return JSON.stringify(cut(operation), null, 2);
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
function rmrf(dir) {
|
|
172
194
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
173
195
|
}
|
|
@@ -233,7 +255,7 @@ async function generateApiPagesForGroup({ contentDir, generatedDocsDir, group, o
|
|
|
233
255
|
manifest.push({ slug, method: operation.method, path: operation.path, tags: operation.tags, title, generated });
|
|
234
256
|
|
|
235
257
|
const opFile = path.join(openapiOutDir, 'operations', operationFileName(operation.method, operation.path));
|
|
236
|
-
fs.writeFileSync(opFile,
|
|
258
|
+
fs.writeFileSync(opFile, operationJson(operation));
|
|
237
259
|
}
|
|
238
260
|
|
|
239
261
|
fs.writeFileSync(path.join(openapiOutDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
@@ -355,7 +377,7 @@ async function parsePageSpecs({ contentDir, openapiOutDir, groupSpecs, summary }
|
|
|
355
377
|
const outDir = path.join(openapiOutDir, '_pages', specDirName(spec), 'operations');
|
|
356
378
|
fs.mkdirSync(outDir, { recursive: true });
|
|
357
379
|
for (const operation of operations) {
|
|
358
|
-
fs.writeFileSync(path.join(outDir, operationFileName(operation.method, operation.path)),
|
|
380
|
+
fs.writeFileSync(path.join(outDir, operationFileName(operation.method, operation.path)), operationJson(operation));
|
|
359
381
|
}
|
|
360
382
|
summary.pageSpecs.push({ spec, operations: operations.length });
|
|
361
383
|
}
|
|
@@ -220,19 +220,9 @@ const darkNavbarFgMuted = darkNavbarConfigured
|
|
|
220
220
|
// design choice, unlike text color above), falling back to the sitewide
|
|
221
221
|
// --wd-primary (today's exact behavior) when unset - a site that sets a
|
|
222
222
|
// navbar background but no accent keeps its ordinary brand-colored
|
|
223
|
-
// active-tab
|
|
223
|
+
// active-tab underline.
|
|
224
224
|
const lightNavbarAccent = navLight.accent ?? lightPrimary;
|
|
225
225
|
const darkNavbarAccent = navDark.accent ?? darkPrimary;
|
|
226
|
-
// --wd-navbar-accent-text: the active tab's own text color, painted on top
|
|
227
|
-
// of --wd-navbar-accent above. Hardcoded to white before this feature
|
|
228
|
-
// existed, which only ever looked right because every accent color in
|
|
229
|
-
// practice (always --wd-primary until now) happened to be dark/saturated
|
|
230
|
-
// enough for white text - contrastTextColor() (lib/config.ts) picks
|
|
231
|
-
// black or white by actual luminance instead, so a light `accent` (e.g.
|
|
232
|
-
// a pale one against a dark navbar) still gets legible active-tab text
|
|
233
|
-
// rather than assuming white always works.
|
|
234
|
-
const lightNavbarAccentText = contrastTextColor(lightNavbarAccent);
|
|
235
|
-
const darkNavbarAccentText = contrastTextColor(darkNavbarAccent);
|
|
236
226
|
// --wd-navbar-border: the switcher pills' (version/language/product) own
|
|
237
227
|
// border. Never the flat --wd-border (a fixed neutral gray/slate tuned for
|
|
238
228
|
// sitting on the page background) once navbar is configured - that reads
|
|
@@ -249,6 +239,14 @@ const lightNavbarBorder = lightNavbarConfigured
|
|
|
249
239
|
const darkNavbarBorder = darkNavbarConfigured
|
|
250
240
|
? `color-mix(in srgb, ${darkNavbarFg} 25%, transparent)`
|
|
251
241
|
: 'var(--wd-border)';
|
|
242
|
+
// --wd-navbar-hover: a topbar tab's background on hover. On a navbar with
|
|
243
|
+
// the page's own background, the sidebar's hover tint (NavTree.astro) -
|
|
244
|
+
// the same 6% of --wd-primary. A configured navbar can *be* the primary
|
|
245
|
+
// color, where that tint would vanish, so there it's a light tint of the
|
|
246
|
+
// navbar's own contrast-correct text color instead, like --wd-navbar-border.
|
|
247
|
+
const sidebarHover = 'color-mix(in srgb, var(--wd-primary) 6%, transparent)';
|
|
248
|
+
const lightNavbarHover = lightNavbarConfigured ? `color-mix(in srgb, ${lightNavbarFg} 12%, transparent)` : sidebarHover;
|
|
249
|
+
const darkNavbarHover = darkNavbarConfigured ? `color-mix(in srgb, ${darkNavbarFg} 12%, transparent)` : sidebarHover;
|
|
252
250
|
// The <body> canvas's own paint - same color as --wd-background above
|
|
253
251
|
// (there's only one background color to configure now), plus an optional
|
|
254
252
|
// image layered on top. `images.light`/`.dark` are plain public/-relative
|
|
@@ -517,8 +515,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
|
|
|
517
515
|
wdNavbarFgLight: lightNavbarFg,
|
|
518
516
|
wdNavbarFgMutedLight: lightNavbarFgMuted,
|
|
519
517
|
wdNavbarAccentLight: lightNavbarAccent,
|
|
520
|
-
wdNavbarAccentTextLight: lightNavbarAccentText,
|
|
521
518
|
wdNavbarBorderLight: lightNavbarBorder,
|
|
519
|
+
wdNavbarHoverLight: lightNavbarHover,
|
|
522
520
|
wdPageBgColorLight: lightPageBgColor,
|
|
523
521
|
wdPageBgImageLight: lightPageBgImage,
|
|
524
522
|
wdFontFamily,
|
|
@@ -531,8 +529,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
|
|
|
531
529
|
wdNavbarFgDark: darkNavbarFg,
|
|
532
530
|
wdNavbarFgMutedDark: darkNavbarFgMuted,
|
|
533
531
|
wdNavbarAccentDark: darkNavbarAccent,
|
|
534
|
-
wdNavbarAccentTextDark: darkNavbarAccentText,
|
|
535
532
|
wdNavbarBorderDark: darkNavbarBorder,
|
|
533
|
+
wdNavbarHoverDark: darkNavbarHover,
|
|
536
534
|
wdPageBgColorDark: darkPageBgColor,
|
|
537
535
|
wdPageBgImageDark: darkPageBgImage,
|
|
538
536
|
}}
|
|
@@ -545,8 +543,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
|
|
|
545
543
|
--wd-navbar-foreground: var(--wdNavbarFgLight);
|
|
546
544
|
--wd-navbar-foreground-muted: var(--wdNavbarFgMutedLight);
|
|
547
545
|
--wd-navbar-accent: var(--wdNavbarAccentLight);
|
|
548
|
-
--wd-navbar-accent-text: var(--wdNavbarAccentTextLight);
|
|
549
546
|
--wd-navbar-border: var(--wdNavbarBorderLight);
|
|
547
|
+
--wd-navbar-hover: var(--wdNavbarHoverLight);
|
|
550
548
|
--wd-page-bg-color: var(--wdPageBgColorLight);
|
|
551
549
|
--wd-page-bg-image: var(--wdPageBgImageLight);
|
|
552
550
|
--wd-text-muted: #64748b;
|
|
@@ -572,8 +570,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
|
|
|
572
570
|
--wd-navbar-foreground: var(--wdNavbarFgDark);
|
|
573
571
|
--wd-navbar-foreground-muted: var(--wdNavbarFgMutedDark);
|
|
574
572
|
--wd-navbar-accent: var(--wdNavbarAccentDark);
|
|
575
|
-
--wd-navbar-accent-text: var(--wdNavbarAccentTextDark);
|
|
576
573
|
--wd-navbar-border: var(--wdNavbarBorderDark);
|
|
574
|
+
--wd-navbar-hover: var(--wdNavbarHoverDark);
|
|
577
575
|
--wd-page-bg-color: var(--wdPageBgColorDark);
|
|
578
576
|
--wd-page-bg-image: var(--wdPageBgImageDark);
|
|
579
577
|
--wd-text-muted: #94a3b8;
|
|
@@ -37,9 +37,8 @@
|
|
|
37
37
|
(below) sits flush against this row's bottom edge (the outer
|
|
38
38
|
.wd-topbar's own border-bottom, since this is the last row) rather
|
|
39
39
|
than floating partway up inside a padded gap. That's what lets a
|
|
40
|
-
plain border-bottom color change read as a real underline
|
|
41
|
-
|
|
42
|
-
attached to the row's bottom edge instead of a floating pill. */
|
|
40
|
+
plain border-bottom color change read as a real underline under the
|
|
41
|
+
active tab. */
|
|
43
42
|
padding-bottom: 0;
|
|
44
43
|
}
|
|
45
44
|
.wd-topbar-brand {
|
|
@@ -139,47 +138,31 @@
|
|
|
139
138
|
/* Rounded top corners, square bottom - reads as an actual "tab"
|
|
140
139
|
attached to the row's bottom edge (see .wd-topbar-row-tabs
|
|
141
140
|
.wd-topbar-inner's own comment) rather than a floating pill. Same
|
|
142
|
-
radius for every tab, active or not
|
|
143
|
-
|
|
144
|
-
below do. */
|
|
141
|
+
radius for every tab, active or not - it also shapes the hover
|
|
142
|
+
background (below). */
|
|
145
143
|
border-radius: 0.5rem 0.5rem 0 0;
|
|
146
|
-
/* Transparent by default -
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
/* Transparent by default - the active state (below) colors it in,
|
|
145
|
+
rather than a separate underline element. Sized to land exactly on the
|
|
146
|
+
row's own bottom edge now that this row's inner wrapper has no bottom
|
|
147
|
+
padding of its own. */
|
|
150
148
|
border-bottom: 2px solid transparent;
|
|
151
149
|
text-decoration: none;
|
|
152
150
|
color: var(--wd-navbar-foreground);
|
|
153
151
|
font-size: 0.92rem;
|
|
154
152
|
font-weight: 600;
|
|
155
153
|
}
|
|
156
|
-
/* The selected tab:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
border-bottom-color matches the fill (rather than being reset to
|
|
161
|
-
transparent) so there's no 2px seam of the row's own background
|
|
162
|
-
showing between the fill and the row's bottom edge/border.
|
|
163
|
-
--wd-navbar-accent (falls back to --wd-primary - see BaseLayout.astro),
|
|
164
|
-
not a bare --wd-primary reference: without this, a site whose navbar
|
|
165
|
-
background *is* --wd-primary (a plausible "brand-colored navbar" choice
|
|
166
|
-
- the exact case that motivated this variable) would have its active
|
|
167
|
-
tab's own fill disappear into the row's background entirely. Text color
|
|
168
|
-
is --wd-navbar-accent-text (also BaseLayout.astro) rather than a
|
|
169
|
-
hardcoded #fff for the matching reason - a light accent color needs
|
|
170
|
-
dark text, not white, to stay legible on top of it. */
|
|
154
|
+
/* The selected tab: just its border-bottom, in --wd-navbar-accent (falls
|
|
155
|
+
back to --wd-primary - see BaseLayout.astro), landing on the row's
|
|
156
|
+
bottom edge as an underline. No fill - the text keeps the navbar's own
|
|
157
|
+
color. */
|
|
171
158
|
.wd-tab-link.active {
|
|
172
|
-
color: var(--wd-navbar-accent-text);
|
|
173
|
-
background: var(--wd-navbar-accent);
|
|
174
159
|
border-bottom-color: var(--wd-navbar-accent);
|
|
175
160
|
}
|
|
176
|
-
/*
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.wd-tab-link:hover:not(.active) {
|
|
182
|
-
border-bottom-color: var(--wd-navbar-accent);
|
|
161
|
+
/* Any tab on hover: a light background tint, the same as a sidebar item's
|
|
162
|
+
hover (see --wd-navbar-hover in BaseLayout.astro). The rounded top
|
|
163
|
+
corners above shape it. */
|
|
164
|
+
.wd-tab-link:hover {
|
|
165
|
+
background: var(--wd-navbar-hover);
|
|
183
166
|
}
|
|
184
167
|
.wd-topbar-links {
|
|
185
168
|
display: flex;
|
package/src/lib/config-schema.js
CHANGED
|
@@ -158,7 +158,7 @@ const stylesSchema = z.object({
|
|
|
158
158
|
// Each side (`light`/`dark`) is either a bare string - just the
|
|
159
159
|
// background color, exactly the original shape, kept for backwards
|
|
160
160
|
// compatibility - or `{ background, accent? }`, for when a site also
|
|
161
|
-
// wants the navbar's own active-tab
|
|
161
|
+
// wants the navbar's own active-tab underline color to
|
|
162
162
|
// differ from the sitewide `styles.colors.primary` (`accent`'s only
|
|
163
163
|
// job - see BaseLayout.astro's lightNavbarAccent). Deliberately no
|
|
164
164
|
// text/icon color field here at all: BaseLayout.astro always resolves
|
package/src/lib/config-schema.ts
CHANGED
|
@@ -263,7 +263,7 @@ const logoSchema = z.union([
|
|
|
263
263
|
// field's comment for what it now covers.
|
|
264
264
|
// One side of `styles.navbar` (light or dark) - either a bare background
|
|
265
265
|
// color (the original shape) or `{ background, accent? }` once a site
|
|
266
|
-
// wants its own accent color (the active
|
|
266
|
+
// wants its own accent color (the active tab's underline)
|
|
267
267
|
// inside the navbar specifically, independent of the sitewide
|
|
268
268
|
// `styles.colors.primary`. Deliberately does NOT carry a text/icon color
|
|
269
269
|
// field at all - see `navbar`'s own comment below (stylesSchema) for why
|
|
@@ -400,7 +400,7 @@ const stylesSchema = z
|
|
|
400
400
|
// Each side (`light`/`dark`) is either a bare string - just the
|
|
401
401
|
// background color, exactly the original shape, kept for backwards
|
|
402
402
|
// compatibility - or `{ background, accent? }`, for when a site also
|
|
403
|
-
// wants the navbar's own active-tab
|
|
403
|
+
// wants the navbar's own active-tab underline color to
|
|
404
404
|
// differ from the sitewide `styles.colors.primary` (`accent`'s only
|
|
405
405
|
// job - see BaseLayout.astro's lightNavbarAccent). Deliberately no
|
|
406
406
|
// text/icon color field here at all: BaseLayout.astro always resolves
|
package/src/lib/content-check.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// - writedocs.json's navigation lists a page that doesn't exist
|
|
11
11
|
// - a redirect is a pattern (`/old/:slug`) rather than one exact path
|
|
12
12
|
// - a Markdown image with a relative path names a file that doesn't exist
|
|
13
|
+
// - an import the build can't resolve: a Docusaurus path (@site/...), or a
|
|
14
|
+
// relative or /snippets/ import of a file that isn't there
|
|
13
15
|
// - an OpenAPI group's spec is missing or doesn't parse, or two groups
|
|
14
16
|
// share one `openapi.path`
|
|
15
17
|
// Warnings - the build succeeds, but not as written:
|
|
@@ -129,6 +131,45 @@ async function checkPage(contentDir, rel, errors, warnings, openapiRefs) {
|
|
|
129
131
|
);
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
134
|
+
// Imports the build can't resolve fail it: Docusaurus' path aliases
|
|
135
|
+
// (common in pages from the previous writedocs), and a relative or
|
|
136
|
+
// /snippets/ import of a file that isn't there. Package imports aren't
|
|
137
|
+
// checked - whether they resolve depends on what's installed.
|
|
138
|
+
visit(tree, 'mdxjsEsm', (node) => {
|
|
139
|
+
const code = String(node.value ?? '');
|
|
140
|
+
for (const m of code.matchAll(/\b(?:import|export)\b[^'";]*?\bfrom\s*['"]([^'"]+)['"]|\bimport\s*['"]([^'"]+)['"]/g)) {
|
|
141
|
+
const spec = m[1] ?? m[2];
|
|
142
|
+
const at = node.position?.start?.line ? node.position.start.line + lineOffset + code.slice(0, m.index).split('\n').length - 1 : undefined;
|
|
143
|
+
if (/^@(site|theme|docusaurus|generated)(\/|$)/.test(spec)) {
|
|
144
|
+
errors.push(
|
|
145
|
+
issue(
|
|
146
|
+
rel,
|
|
147
|
+
at,
|
|
148
|
+
`Imports "${spec}", a Docusaurus path - the build fails on it.`,
|
|
149
|
+
/^@site\/src\/components\/?$/.test(spec)
|
|
150
|
+
? "writedocs' components (Card, Callout, Tabs, Accordion, ...) need no import - remove this line."
|
|
151
|
+
: spec.startsWith('@site/src/components/')
|
|
152
|
+
? 'A custom component of the old site - move it into a snippet (snippets/), or remove it and what uses it.'
|
|
153
|
+
: 'Remove the import, and what uses it.'
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let target = null;
|
|
159
|
+
if (spec.startsWith('./') || spec.startsWith('../')) target = path.resolve(path.dirname(path.join(contentDir, rel)), spec);
|
|
160
|
+
else if (spec.startsWith('/snippets/')) target = path.join(contentDir, spec);
|
|
161
|
+
if (!target) continue;
|
|
162
|
+
const exists = ['', '.mdx', '.md', '.js', '.jsx', '.ts', '.tsx', '.json'].some((ext) => {
|
|
163
|
+
try {
|
|
164
|
+
return fs.statSync(target + ext).isFile();
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
if (!exists) errors.push(issue(rel, at, `Imports ${spec}, which doesn't exist - the build fails on it.`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
132
173
|
// A Markdown image with a relative path is imported by the build (Astro's
|
|
133
174
|
// image pipeline), so a missing file fails the whole build.
|
|
134
175
|
visit(tree, 'image', (node) => {
|