docula 1.13.0 → 2.0.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/README.md +3 -3
- package/dist/docula.d.ts +3 -0
- package/dist/docula.js +90 -42
- package/package.json +20 -24
- package/templates/classic/includes/header.hbs +1 -1
- package/templates/modern/api.hbs +1 -1
- package/templates/modern/css/api.css +22 -4
- package/templates/modern/css/styles.css +39 -1
- package/templates/modern/includes/header.hbs +1 -1
- package/templates/modern/includes/scripts.hbs +39 -0
- package/templates/modern/includes/sidebar.hbs +7 -2
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ You can build Docula as a standalone binary that runs without Node.js installed.
|
|
|
56
56
|
|
|
57
57
|
## Building the Binary
|
|
58
58
|
|
|
59
|
-
Requires Node.js >=
|
|
59
|
+
Requires Node.js >= 25.7.0 to build (the resulting binary does not need Node.js to run). The build uses [tsdown's `exe` option](https://tsdown.dev/options/exe), which wraps Node.js's [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) feature added in Node 25.7.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
pnpm install
|
|
@@ -69,7 +69,7 @@ This produces a platform-specific binary at `dist/docula` (or `dist/docula.exe`
|
|
|
69
69
|
|
|
70
70
|
1. Embeds all built-in templates (modern, classic) into the bundle as base64
|
|
71
71
|
2. Bundles all source code and dependencies into a single CJS file via [tsdown](https://tsdown.dev/)
|
|
72
|
-
3. Uses tsdown's built-in `exe` option to create a Node.js SEA binary
|
|
72
|
+
3. Uses tsdown's built-in `exe` option to create a Node.js SEA binary — blob generation, injection, and (on macOS hosts) ad-hoc code signing are handled automatically
|
|
73
73
|
|
|
74
74
|
## Testing the Binary
|
|
75
75
|
|
|
@@ -91,7 +91,7 @@ After building, test it locally:
|
|
|
91
91
|
|
|
92
92
|
## Cross-Platform Binaries
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
The CI workflow (`.github/workflows/build-binaries.yaml`) builds each platform natively on its own runner. Native builds avoid the cross-compile signing pitfall on Apple Silicon, where unsigned Mach-O binaries are killed on launch.
|
|
95
95
|
|
|
96
96
|
| Platform | Runner | Artifact |
|
|
97
97
|
|---|---|---|
|
package/dist/docula.d.ts
CHANGED
|
@@ -83,6 +83,7 @@ type ApiOperation = {
|
|
|
83
83
|
id: string;
|
|
84
84
|
method: string;
|
|
85
85
|
methodUpper: string;
|
|
86
|
+
methodShort: string;
|
|
86
87
|
path: string;
|
|
87
88
|
summary: string;
|
|
88
89
|
description: string;
|
|
@@ -214,6 +215,7 @@ type DoculaData = {
|
|
|
214
215
|
changelogUrl: string;
|
|
215
216
|
editPageUrl?: string;
|
|
216
217
|
openGraph?: DoculaOpenGraph;
|
|
218
|
+
faviconUrl?: string;
|
|
217
219
|
};
|
|
218
220
|
type DoculaTemplates = {
|
|
219
221
|
home: string;
|
|
@@ -448,6 +450,7 @@ declare class DoculaBuilder {
|
|
|
448
450
|
private readonly _console;
|
|
449
451
|
private readonly _hash;
|
|
450
452
|
onReleaseChangelog?: (entries: DoculaChangelogEntry[], console: DoculaConsole) => Promise<DoculaChangelogEntry[]> | DoculaChangelogEntry[];
|
|
453
|
+
onAutoReadme?: (content: string, sourcePath: string, console: DoculaConsole) => Promise<string> | string;
|
|
451
454
|
get console(): DoculaConsole;
|
|
452
455
|
constructor(options?: DoculaBuilderOptions, engineOptions?: any);
|
|
453
456
|
get options(): DoculaOptions;
|
package/dist/docula.js
CHANGED
|
@@ -11,8 +11,9 @@ import { Writr, Writr as Writr$1 } from "writr";
|
|
|
11
11
|
import { blue, bold, cyan, dim, gray, green, magenta, red, white, yellow } from "colorette";
|
|
12
12
|
import { CacheableNet } from "@cacheable/net";
|
|
13
13
|
import os from "node:os";
|
|
14
|
+
import sea from "node:sea";
|
|
14
15
|
//#region package.json
|
|
15
|
-
var version = "
|
|
16
|
+
var version = "2.0.0";
|
|
16
17
|
var package_default = {
|
|
17
18
|
name: "docula",
|
|
18
19
|
version,
|
|
@@ -27,12 +28,12 @@ var package_default = {
|
|
|
27
28
|
"url": "git+https://github.com/jaredwray/docula.git"
|
|
28
29
|
},
|
|
29
30
|
author: "Jared Wray <me@jaredwray.com>",
|
|
30
|
-
engines: { "node": ">=
|
|
31
|
+
engines: { "node": "^22.18.0 || >=24.0.0" },
|
|
31
32
|
license: "MIT",
|
|
32
33
|
scripts: {
|
|
33
34
|
"clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/dist",
|
|
34
35
|
"build": "pnpm generate-init-file && tsdown",
|
|
35
|
-
"build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts
|
|
36
|
+
"build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts",
|
|
36
37
|
"generate-embedded-templates": "tsx scripts/generate-embedded-templates.ts",
|
|
37
38
|
"lint": "biome check --write --error-on-warnings",
|
|
38
39
|
"lint:ci": "biome check --error-on-warnings",
|
|
@@ -69,42 +70,39 @@ var package_default = {
|
|
|
69
70
|
],
|
|
70
71
|
bin: { "docula": "./bin/docula.js" },
|
|
71
72
|
dependencies: {
|
|
72
|
-
"@ai-sdk/anthropic": "^3.0.
|
|
73
|
-
"@ai-sdk/google": "^3.0.
|
|
74
|
-
"@ai-sdk/openai": "^3.0.
|
|
75
|
-
"@cacheable/net": "^2.0.
|
|
76
|
-
"ai": "^6.0.
|
|
73
|
+
"@ai-sdk/anthropic": "^3.0.77",
|
|
74
|
+
"@ai-sdk/google": "^3.0.72",
|
|
75
|
+
"@ai-sdk/openai": "^3.0.63",
|
|
76
|
+
"@cacheable/net": "^2.0.7",
|
|
77
|
+
"ai": "^6.0.178",
|
|
77
78
|
"colorette": "^2.0.20",
|
|
78
|
-
"ecto": "^4.8.
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"jiti": "^2.6.1",
|
|
79
|
+
"ecto": "^4.8.5",
|
|
80
|
+
"hashery": "^2.0.0",
|
|
81
|
+
"jiti": "^2.7.0",
|
|
82
82
|
"serve-handler": "^6.1.7",
|
|
83
83
|
"update-notifier": "^7.3.1",
|
|
84
|
-
"writr": "^6.1.
|
|
84
|
+
"writr": "^6.1.2"
|
|
85
85
|
},
|
|
86
86
|
devDependencies: {
|
|
87
|
-
"@biomejs/biome": "^2.4.
|
|
88
|
-
"@playwright/test": "^1.
|
|
89
|
-
"@types/
|
|
90
|
-
"@types/js-yaml": "^4.0.9",
|
|
91
|
-
"@types/node": "^25.5.0",
|
|
87
|
+
"@biomejs/biome": "^2.4.15",
|
|
88
|
+
"@playwright/test": "^1.60.0",
|
|
89
|
+
"@types/node": "^24.12.4",
|
|
92
90
|
"@types/serve-handler": "^6.1.4",
|
|
93
91
|
"@types/update-notifier": "^6.0.8",
|
|
94
|
-
"@vitest/coverage-v8": "^4.1.
|
|
95
|
-
"dotenv": "^17.
|
|
96
|
-
"postject": "1.0.0-alpha.6",
|
|
92
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
93
|
+
"dotenv": "^17.4.2",
|
|
97
94
|
"rimraf": "^6.1.3",
|
|
98
|
-
"tsdown": "^0.
|
|
95
|
+
"tsdown": "^0.22.0",
|
|
99
96
|
"tsx": "^4.21.0",
|
|
100
|
-
"typescript": "^
|
|
101
|
-
"vitest": "^4.1.
|
|
97
|
+
"typescript": "^6.0.3",
|
|
98
|
+
"vitest": "^4.1.6"
|
|
102
99
|
},
|
|
103
100
|
files: [
|
|
104
101
|
"dist",
|
|
105
102
|
"templates",
|
|
106
103
|
"bin"
|
|
107
|
-
]
|
|
104
|
+
],
|
|
105
|
+
packageManager: "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d"
|
|
108
106
|
};
|
|
109
107
|
//#endregion
|
|
110
108
|
//#region src/builder-ai.ts
|
|
@@ -121,15 +119,15 @@ async function createAIModel(ai) {
|
|
|
121
119
|
switch (ai.provider) {
|
|
122
120
|
case "anthropic": {
|
|
123
121
|
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
124
|
-
return createAnthropic({ apiKey: ai.apiKey })(ai.model
|
|
122
|
+
return createAnthropic(ai.apiKey ? { apiKey: ai.apiKey } : {})(ai.model || "claude-haiku-4-5");
|
|
125
123
|
}
|
|
126
124
|
case "openai": {
|
|
127
125
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
128
|
-
return createOpenAI({ apiKey: ai.apiKey })(ai.model
|
|
126
|
+
return createOpenAI(ai.apiKey ? { apiKey: ai.apiKey } : {})(ai.model || "gpt-4o-mini");
|
|
129
127
|
}
|
|
130
128
|
case "google": {
|
|
131
129
|
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
132
|
-
return createGoogleGenerativeAI({ apiKey: ai.apiKey })(ai.model
|
|
130
|
+
return createGoogleGenerativeAI(ai.apiKey ? { apiKey: ai.apiKey } : {})(ai.model || "gemini-2.5-flash-lite");
|
|
133
131
|
}
|
|
134
132
|
default: return;
|
|
135
133
|
}
|
|
@@ -379,10 +377,13 @@ function parseOpenApiSpec(specJson) {
|
|
|
379
377
|
const requestBody = extractRequestBody(operation, spec);
|
|
380
378
|
const responses = extractResponses(operation, spec);
|
|
381
379
|
const codeExamples = generateCodeExamples(method, pathStr, servers.length > 0 ? servers[0].url : "", parameters, requestBody);
|
|
380
|
+
const operationId = operation.operationId ?? `${method}-${pathStr.replaceAll(/[^a-zA-Z0-9]/g, "-")}`;
|
|
381
|
+
const methodUpper = method.toUpperCase();
|
|
382
382
|
const apiOperation = {
|
|
383
|
-
id: slugify(
|
|
383
|
+
id: slugify(operationId),
|
|
384
384
|
method,
|
|
385
|
-
methodUpper
|
|
385
|
+
methodUpper,
|
|
386
|
+
methodShort: methodUpper === "DELETE" ? "DEL" : methodUpper === "OPTIONS" ? "OPT" : methodUpper,
|
|
386
387
|
path: pathStr,
|
|
387
388
|
summary: operation.summary ?? "",
|
|
388
389
|
description: operation.description ?? "",
|
|
@@ -597,6 +598,7 @@ function extractResponses(operation, spec) {
|
|
|
597
598
|
responses.push({
|
|
598
599
|
statusCode,
|
|
599
600
|
statusClass: getStatusClass(statusCode),
|
|
601
|
+
/* v8 ignore next */
|
|
600
602
|
description: response.description ?? "",
|
|
601
603
|
contentType,
|
|
602
604
|
schemaProperties,
|
|
@@ -1093,6 +1095,22 @@ function hashFile(hash, filePath) {
|
|
|
1093
1095
|
const content = fs.readFileSync(filePath);
|
|
1094
1096
|
return hash.toHashSync(content);
|
|
1095
1097
|
}
|
|
1098
|
+
function tryHashFile(hash, filePath) {
|
|
1099
|
+
try {
|
|
1100
|
+
return hashFile(hash, filePath);
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (error.code === "ENOENT") return;
|
|
1103
|
+
/* v8 ignore next 2 -- @preserve */
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function hashConfigFile(hash, sitePath) {
|
|
1108
|
+
for (const name of ["docula.config.ts", "docula.config.mjs"]) {
|
|
1109
|
+
const configPath = path.join(sitePath, name);
|
|
1110
|
+
if (fs.existsSync(configPath)) return hashFile(hash, configPath);
|
|
1111
|
+
}
|
|
1112
|
+
return "";
|
|
1113
|
+
}
|
|
1096
1114
|
function hashOptions(hash, options) {
|
|
1097
1115
|
const relevant = {
|
|
1098
1116
|
siteUrl: options.siteUrl,
|
|
@@ -1119,19 +1137,29 @@ function hashOptions(hash, options) {
|
|
|
1119
1137
|
ai: options.ai,
|
|
1120
1138
|
googleTagManager: options.googleTagManager
|
|
1121
1139
|
};
|
|
1122
|
-
|
|
1140
|
+
const optionsHash = hash.toHashSync(JSON.stringify(relevant));
|
|
1141
|
+
const configHash = hashConfigFile(hash, options.sitePath);
|
|
1142
|
+
return hash.toHashSync(`${optionsHash}:${configHash}`);
|
|
1123
1143
|
}
|
|
1124
1144
|
function hashTemplateDirectory(hash, templatePath) {
|
|
1125
1145
|
/* v8 ignore next 3 -- @preserve */
|
|
1126
1146
|
if (!fs.existsSync(templatePath)) return "";
|
|
1127
|
-
const
|
|
1147
|
+
const files = listFilesRecursive(templatePath);
|
|
1148
|
+
const hashes = [];
|
|
1149
|
+
for (const f of files) {
|
|
1150
|
+
const fileHash = tryHashFile(hash, path.join(templatePath, f));
|
|
1151
|
+
if (fileHash !== void 0) hashes.push(fileHash);
|
|
1152
|
+
}
|
|
1128
1153
|
return hash.toHashSync(hashes.join(""));
|
|
1129
1154
|
}
|
|
1130
1155
|
function hashSourceFiles(hash, dir) {
|
|
1131
1156
|
const hashes = {};
|
|
1132
1157
|
if (!fs.existsSync(dir)) return hashes;
|
|
1133
1158
|
const files = listFilesRecursive(dir);
|
|
1134
|
-
for (const file of files)
|
|
1159
|
+
for (const file of files) {
|
|
1160
|
+
const fileHash = tryHashFile(hash, path.join(dir, file));
|
|
1161
|
+
if (fileHash !== void 0) hashes[file] = fileHash;
|
|
1162
|
+
}
|
|
1135
1163
|
return hashes;
|
|
1136
1164
|
}
|
|
1137
1165
|
function recordsEqual(a, b) {
|
|
@@ -1145,6 +1173,7 @@ function hasAssetsChanged(hash, sitePath, previousAssets, autoReadme) {
|
|
|
1145
1173
|
for (const file of [
|
|
1146
1174
|
"favicon.ico",
|
|
1147
1175
|
"logo.svg",
|
|
1176
|
+
"logo.png",
|
|
1148
1177
|
"logo_horizontal.png",
|
|
1149
1178
|
"variables.css",
|
|
1150
1179
|
"api/swagger.json",
|
|
@@ -2669,12 +2698,7 @@ var DoculaOptions = class {
|
|
|
2669
2698
|
* Returns true when running as a single-executable application (SEA).
|
|
2670
2699
|
*/
|
|
2671
2700
|
function isSEA() {
|
|
2672
|
-
|
|
2673
|
-
return Boolean(process.sea);
|
|
2674
|
-
} catch {
|
|
2675
|
-
/* v8 ignore next -- @preserve */
|
|
2676
|
-
return false;
|
|
2677
|
-
}
|
|
2701
|
+
return sea.isSea();
|
|
2678
2702
|
}
|
|
2679
2703
|
/**
|
|
2680
2704
|
* Returns the deterministic temp directory path for extracted templates.
|
|
@@ -2739,6 +2763,7 @@ var DoculaBuilder = class {
|
|
|
2739
2763
|
_console;
|
|
2740
2764
|
_hash = new Hashery();
|
|
2741
2765
|
onReleaseChangelog;
|
|
2766
|
+
onAutoReadme;
|
|
2742
2767
|
get console() {
|
|
2743
2768
|
return this._console;
|
|
2744
2769
|
}
|
|
@@ -2804,6 +2829,12 @@ var DoculaBuilder = class {
|
|
|
2804
2829
|
editPageUrl: this.options.editPageUrl,
|
|
2805
2830
|
openGraph: this.options.openGraph
|
|
2806
2831
|
};
|
|
2832
|
+
const resolvedFavicon = [
|
|
2833
|
+
"favicon.ico",
|
|
2834
|
+
"logo.svg",
|
|
2835
|
+
"logo.png"
|
|
2836
|
+
].find((file) => fs.existsSync(path.join(this.options.sitePath, file)));
|
|
2837
|
+
if (resolvedFavicon) doculaData.faviconUrl = buildUrlPath(this.options.baseUrl, resolvedFavicon);
|
|
2807
2838
|
if (siteReadmeExists) currentAssetHashes["README.md"] = hashFile(this._hash, path.join(this.options.sitePath, "README.md"));
|
|
2808
2839
|
else if (autoReadmeResult) currentAssetHashes.__autoReadme = hashFile(this._hash, autoReadmeResult.sourcePath);
|
|
2809
2840
|
if (Array.isArray(this.options.openApiUrl)) doculaData.openApiSpecs = this.options.openApiUrl.map((spec) => ({
|
|
@@ -2939,6 +2970,10 @@ var DoculaBuilder = class {
|
|
|
2939
2970
|
await fs.promises.copyFile(`${siteRelativePath}/logo.svg`, `${this.options.output}/logo.svg`);
|
|
2940
2971
|
this._console.fileCopied("logo.svg");
|
|
2941
2972
|
}
|
|
2973
|
+
if (!hashAssetAndCheckSkip(this._hash, path.join(siteRelativePath, "logo.png"), path.join(this.options.output, "logo.png"), "logo.png", previousAssets, currentAssetHashes)) {
|
|
2974
|
+
await fs.promises.copyFile(path.join(siteRelativePath, "logo.png"), path.join(this.options.output, "logo.png"));
|
|
2975
|
+
this._console.fileCopied("logo.png");
|
|
2976
|
+
}
|
|
2942
2977
|
if (!hashAssetAndCheckSkip(this._hash, `${siteRelativePath}/logo_horizontal.png`, `${this.options.output}/logo_horizontal.png`, "logo_horizontal.png", previousAssets, currentAssetHashes)) {
|
|
2943
2978
|
await fs.promises.copyFile(`${siteRelativePath}/logo_horizontal.png`, `${this.options.output}/logo_horizontal.png`);
|
|
2944
2979
|
this._console.fileCopied("logo_horizontal.png");
|
|
@@ -3014,9 +3049,16 @@ var DoculaBuilder = class {
|
|
|
3014
3049
|
if (packageJson.name && typeof packageJson.name === "string") readmeContent = `# ${packageJson.name}\n\n${readmeContent}`;
|
|
3015
3050
|
} catch {}
|
|
3016
3051
|
}
|
|
3052
|
+
let content = readmeContent;
|
|
3053
|
+
if (this.onAutoReadme) try {
|
|
3054
|
+
content = await this.onAutoReadme(content, cwdReadmePath, this._console);
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3057
|
+
this._console.error(`onAutoReadme error: ${message}`);
|
|
3058
|
+
}
|
|
3017
3059
|
return {
|
|
3018
3060
|
sourcePath: cwdReadmePath,
|
|
3019
|
-
content
|
|
3061
|
+
content
|
|
3020
3062
|
};
|
|
3021
3063
|
}
|
|
3022
3064
|
async getGithubData(githubPath) {
|
|
@@ -3396,6 +3438,8 @@ var Docula = class {
|
|
|
3396
3438
|
const builder = new DoculaBuilder(Object.assign(this.options, { console: this._console }));
|
|
3397
3439
|
/* v8 ignore next 4 -- @preserve */
|
|
3398
3440
|
if (this._configFileModule.onReleaseChangelog) builder.onReleaseChangelog = this._configFileModule.onReleaseChangelog;
|
|
3441
|
+
/* v8 ignore next 4 -- @preserve */
|
|
3442
|
+
if (this._configFileModule.onAutoReadme) builder.onAutoReadme = this._configFileModule.onAutoReadme;
|
|
3399
3443
|
await builder.build();
|
|
3400
3444
|
return builder;
|
|
3401
3445
|
}
|
|
@@ -3496,12 +3540,16 @@ var Docula = class {
|
|
|
3496
3540
|
}
|
|
3497
3541
|
else {
|
|
3498
3542
|
const { createJiti } = await import("jiti");
|
|
3499
|
-
|
|
3543
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
3544
|
+
this._configFileModule = await jiti.import(absolutePath);
|
|
3500
3545
|
}
|
|
3501
3546
|
return;
|
|
3502
3547
|
}
|
|
3503
3548
|
/* v8 ignore next -- @preserve */
|
|
3504
|
-
if (fs.existsSync(mjsConfigFile))
|
|
3549
|
+
if (fs.existsSync(mjsConfigFile)) {
|
|
3550
|
+
const absolutePath = path.resolve(mjsConfigFile);
|
|
3551
|
+
this._configFileModule = await import(pathToFileURL(absolutePath).href);
|
|
3552
|
+
}
|
|
3505
3553
|
}
|
|
3506
3554
|
/**
|
|
3507
3555
|
* Watch the site path for file changes and rebuild on change
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docula",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Beautiful Website for Your Projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/docula.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"author": "Jared Wray <me@jaredwray.com>",
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": "^22.18.0 || >=24.0.0"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"keywords": [
|
|
@@ -40,36 +40,32 @@
|
|
|
40
40
|
"docula": "./bin/docula.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ai-sdk/anthropic": "^3.0.
|
|
44
|
-
"@ai-sdk/google": "^3.0.
|
|
45
|
-
"@ai-sdk/openai": "^3.0.
|
|
46
|
-
"@cacheable/net": "^2.0.
|
|
47
|
-
"ai": "^6.0.
|
|
43
|
+
"@ai-sdk/anthropic": "^3.0.77",
|
|
44
|
+
"@ai-sdk/google": "^3.0.72",
|
|
45
|
+
"@ai-sdk/openai": "^3.0.63",
|
|
46
|
+
"@cacheable/net": "^2.0.7",
|
|
47
|
+
"ai": "^6.0.178",
|
|
48
48
|
"colorette": "^2.0.20",
|
|
49
|
-
"ecto": "^4.8.
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"jiti": "^2.6.1",
|
|
49
|
+
"ecto": "^4.8.5",
|
|
50
|
+
"hashery": "^2.0.0",
|
|
51
|
+
"jiti": "^2.7.0",
|
|
53
52
|
"serve-handler": "^6.1.7",
|
|
54
53
|
"update-notifier": "^7.3.1",
|
|
55
|
-
"writr": "^6.1.
|
|
54
|
+
"writr": "^6.1.2"
|
|
56
55
|
},
|
|
57
56
|
"devDependencies": {
|
|
58
|
-
"@biomejs/biome": "^2.4.
|
|
59
|
-
"@playwright/test": "^1.
|
|
60
|
-
"@types/
|
|
61
|
-
"@types/js-yaml": "^4.0.9",
|
|
62
|
-
"@types/node": "^25.5.0",
|
|
57
|
+
"@biomejs/biome": "^2.4.15",
|
|
58
|
+
"@playwright/test": "^1.60.0",
|
|
59
|
+
"@types/node": "^24.12.4",
|
|
63
60
|
"@types/serve-handler": "^6.1.4",
|
|
64
61
|
"@types/update-notifier": "^6.0.8",
|
|
65
|
-
"@vitest/coverage-v8": "^4.1.
|
|
66
|
-
"dotenv": "^17.
|
|
67
|
-
"postject": "1.0.0-alpha.6",
|
|
62
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
63
|
+
"dotenv": "^17.4.2",
|
|
68
64
|
"rimraf": "^6.1.3",
|
|
69
|
-
"tsdown": "^0.
|
|
65
|
+
"tsdown": "^0.22.0",
|
|
70
66
|
"tsx": "^4.21.0",
|
|
71
|
-
"typescript": "^
|
|
72
|
-
"vitest": "^4.1.
|
|
67
|
+
"typescript": "^6.0.3",
|
|
68
|
+
"vitest": "^4.1.6"
|
|
73
69
|
},
|
|
74
70
|
"files": [
|
|
75
71
|
"dist",
|
|
@@ -79,7 +75,7 @@
|
|
|
79
75
|
"scripts": {
|
|
80
76
|
"clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/dist",
|
|
81
77
|
"build": "pnpm generate-init-file && tsdown",
|
|
82
|
-
"build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts
|
|
78
|
+
"build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts",
|
|
83
79
|
"generate-embedded-templates": "tsx scripts/generate-embedded-templates.ts",
|
|
84
80
|
"lint": "biome check --write --error-on-warnings",
|
|
85
81
|
"lint:ci": "biome check --error-on-warnings",
|
|
@@ -31,4 +31,4 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
|
31
31
|
rel="stylesheet"
|
|
32
32
|
/>
|
|
33
33
|
<link rel="stylesheet" href="/css/highlight/styles/base16/dracula.min.css" />
|
|
34
|
-
<link rel="icon" href="
|
|
34
|
+
{{#if faviconUrl}}<link rel="icon" href="{{faviconUrl}}" />{{/if}}
|
package/templates/modern/api.hbs
CHANGED
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
<div class="api-sidebar__group-items">
|
|
40
40
|
{{#each this.operations}}
|
|
41
41
|
<a href="#{{this.id}}" class="api-sidebar__item" data-method="{{this.method}}" data-path="{{this.path}}">
|
|
42
|
-
<span class="method-badge method-badge--{{this.method}}">{{this.methodUpper}}</span>
|
|
43
42
|
<span class="api-sidebar__item-path">{{this.path}}</span>
|
|
43
|
+
<span class="method-badge method-badge--{{this.method}}">{{this.methodShort}}</span>
|
|
44
44
|
</a>
|
|
45
45
|
{{/each}}
|
|
46
46
|
</div>
|
|
@@ -100,17 +100,20 @@
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.api-sidebar__group-items {
|
|
103
|
-
padding:
|
|
103
|
+
padding: 4px 0 8px 12px;
|
|
104
|
+
margin-left: 10px;
|
|
105
|
+
border-left: 1px solid var(--border);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
.api-sidebar__item {
|
|
107
109
|
display: flex;
|
|
108
110
|
align-items: center;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
gap: 12px;
|
|
113
|
+
padding: 8px 10px;
|
|
111
114
|
font-size: 13px;
|
|
112
115
|
border-radius: 4px;
|
|
113
|
-
color: var(--fg);
|
|
116
|
+
color: var(--muted-fg);
|
|
114
117
|
white-space: nowrap;
|
|
115
118
|
overflow: hidden;
|
|
116
119
|
text-overflow: ellipsis;
|
|
@@ -125,8 +128,23 @@
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
.api-sidebar__item-path {
|
|
131
|
+
min-width: 0;
|
|
128
132
|
overflow: hidden;
|
|
129
133
|
text-overflow: ellipsis;
|
|
134
|
+
color: var(--muted-fg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.api-sidebar__item--active .api-sidebar__item-path {
|
|
138
|
+
color: var(--fg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.api-sidebar__item .method-badge {
|
|
142
|
+
background: transparent;
|
|
143
|
+
padding: 0;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
border-radius: 0;
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
letter-spacing: 0.5px;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
/* Method Badges */
|
|
@@ -289,6 +289,44 @@ body {
|
|
|
289
289
|
letter-spacing: 0.24px;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
.nav-sidebar__title:has(.nav-sidebar__toggle) {
|
|
293
|
+
padding: 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.nav-sidebar__toggle {
|
|
297
|
+
all: unset;
|
|
298
|
+
box-sizing: border-box;
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: space-between;
|
|
302
|
+
width: 100%;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
padding: 0 8px 0 10px;
|
|
305
|
+
font: inherit;
|
|
306
|
+
color: inherit;
|
|
307
|
+
text-transform: inherit;
|
|
308
|
+
letter-spacing: inherit;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.nav-sidebar__toggle:focus-visible {
|
|
312
|
+
outline: 2px solid var(--accent, currentColor);
|
|
313
|
+
outline-offset: 2px;
|
|
314
|
+
border-radius: 4px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.nav-sidebar__chevron {
|
|
318
|
+
flex-shrink: 0;
|
|
319
|
+
transition: transform 150ms ease;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.nav-sidebar__section--collapsed .nav-sidebar__chevron {
|
|
323
|
+
transform: rotate(-90deg);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.nav-sidebar__section--collapsed .nav-sidebar__list {
|
|
327
|
+
display: none;
|
|
328
|
+
}
|
|
329
|
+
|
|
292
330
|
.nav-sidebar__list {
|
|
293
331
|
margin-block: 2px;
|
|
294
332
|
}
|
|
@@ -360,7 +398,7 @@ body {
|
|
|
360
398
|
.copy-code-btn { position: absolute; top: 8px; right: 8px; padding: 4px; line-height: 0; border-radius: 4px; background: transparent; color: var(--muted); border: none; cursor: pointer; opacity: 0; transition: opacity 0.15s; }
|
|
361
399
|
pre:hover .copy-code-btn { opacity: 1; }
|
|
362
400
|
.copy-code-btn:hover { color: var(--fg); }
|
|
363
|
-
.article__main img, .changelog-entry-body img { cursor: zoom-in; }
|
|
401
|
+
.article__main img:not(a img), .changelog-entry-body img:not(a img) { cursor: zoom-in; }
|
|
364
402
|
.lightbox-overlay { display: none; position: fixed; inset: 0; z-index: 200; background: rgba(0, 0, 0, 0.8); justify-content: center; align-items: center; cursor: pointer; }
|
|
365
403
|
.lightbox-overlay--visible { display: flex !important; }
|
|
366
404
|
.lightbox-overlay img { max-width: 90vw; max-height: 90vh; border-radius: 8px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); cursor: default; }
|
|
@@ -22,7 +22,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
|
22
22
|
<link rel="stylesheet" href="{{baseUrl}}/css/variables.css">
|
|
23
23
|
<link rel="stylesheet" href="{{baseUrl}}/css/styles.css">
|
|
24
24
|
<link rel="stylesheet" href="{{baseUrl}}/css/highlight/styles/base16/docula.css">
|
|
25
|
-
<link rel="icon" href="{{
|
|
25
|
+
{{#if faviconUrl}}<link rel="icon" href="{{faviconUrl}}">{{/if}}
|
|
26
26
|
<script>
|
|
27
27
|
(function(){
|
|
28
28
|
window.__doculaThemeKey = 'docula:theme:' + ({{#if siteUrl}}'{{siteUrl}}'{{else}}location.origin{{/if}}).replace(/^https?:\/\//, '');
|
|
@@ -227,6 +227,44 @@
|
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
// Sidebar section collapse/expand
|
|
231
|
+
const SIDEBAR_STORAGE_KEY = 'docula:sidebar-sections';
|
|
232
|
+
const collapsibleSections = document.querySelectorAll('.nav-sidebar__section--collapsible');
|
|
233
|
+
if (collapsibleSections.length > 0) {
|
|
234
|
+
let storedSectionState = {};
|
|
235
|
+
try {
|
|
236
|
+
storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
|
|
237
|
+
} catch (e) { storedSectionState = {}; }
|
|
238
|
+
|
|
239
|
+
const setSectionOpen = (section, toggle, open) => {
|
|
240
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
241
|
+
section.classList.toggle('nav-sidebar__section--collapsed', !open);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
collapsibleSections.forEach((section, idx) => {
|
|
245
|
+
const toggle = section.querySelector('.nav-sidebar__toggle');
|
|
246
|
+
const list = section.querySelector('.nav-sidebar__list');
|
|
247
|
+
if (!toggle || !list) return;
|
|
248
|
+
const listId = 'nav-sidebar-section-' + idx;
|
|
249
|
+
list.id = listId;
|
|
250
|
+
toggle.setAttribute('aria-controls', listId);
|
|
251
|
+
|
|
252
|
+
const key = 'section-' + idx;
|
|
253
|
+
const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
|
|
254
|
+
const isOpen = hasStored ? !!storedSectionState[key] : true;
|
|
255
|
+
setSectionOpen(section, toggle, isOpen);
|
|
256
|
+
|
|
257
|
+
toggle.addEventListener('click', () => {
|
|
258
|
+
const next = toggle.getAttribute('aria-expanded') !== 'true';
|
|
259
|
+
setSectionOpen(section, toggle, next);
|
|
260
|
+
storedSectionState[key] = next;
|
|
261
|
+
try {
|
|
262
|
+
localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(storedSectionState));
|
|
263
|
+
} catch (e) { /* storage unavailable */ }
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
230
268
|
// Active header nav link highlighting
|
|
231
269
|
const navLinks = document.querySelectorAll('.header-bottom__item');
|
|
232
270
|
navLinks.forEach((link) => {
|
|
@@ -282,6 +320,7 @@
|
|
|
282
320
|
document.body.appendChild(lightboxOverlay);
|
|
283
321
|
|
|
284
322
|
document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
|
|
323
|
+
if (img.closest('a')) return;
|
|
285
324
|
img.addEventListener('click', function() {
|
|
286
325
|
lightboxImg.src = img.src;
|
|
287
326
|
lightboxOverlay.classList.add('lightbox-overlay--visible');
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{{#forEach sidebarItems}}
|
|
2
2
|
{{#if children}}
|
|
3
|
-
<section class="nav-sidebar__section">
|
|
4
|
-
<h2 class="nav-sidebar__title">
|
|
3
|
+
<section class="nav-sidebar__section nav-sidebar__section--collapsible">
|
|
4
|
+
<h2 class="nav-sidebar__title">
|
|
5
|
+
<button type="button" class="nav-sidebar__toggle" aria-expanded="true">
|
|
6
|
+
<span class="nav-sidebar__toggle-label">{{name}}</span>
|
|
7
|
+
<svg class="nav-sidebar__chevron" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
|
|
8
|
+
</button>
|
|
9
|
+
</h2>
|
|
5
10
|
<ul class="nav-sidebar__list">
|
|
6
11
|
{{#forEach children}}
|
|
7
12
|
<li>
|