jsgui3-server 0.0.151 → 0.0.152

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.
Files changed (77) hide show
  1. package/README.md +21 -0
  2. package/admin-ui/v1/controls/admin_shell.js +33 -0
  3. package/admin-ui/v1/server.js +14 -1
  4. package/docs/api-reference.md +120 -2
  5. package/docs/books/website-design/01-introduction.md +73 -0
  6. package/docs/books/website-design/02-current-state.md +195 -0
  7. package/docs/books/website-design/03-base-class.md +181 -0
  8. package/docs/books/website-design/04-webpage.md +307 -0
  9. package/docs/books/website-design/05-website.md +456 -0
  10. package/docs/books/website-design/06-pages-storage.md +170 -0
  11. package/docs/books/website-design/07-api-layer.md +285 -0
  12. package/docs/books/website-design/08-server-integration.md +271 -0
  13. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  14. package/docs/books/website-design/10-open-questions.md +196 -0
  15. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  16. package/docs/books/website-design/12-content-model.md +395 -0
  17. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  18. package/docs/books/website-design/14-website-module-spec.md +541 -0
  19. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  20. package/docs/books/website-design/16-minimal-first.md +203 -0
  21. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  22. package/docs/books/website-design/README.md +43 -0
  23. package/docs/configuration-reference.md +54 -0
  24. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  25. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  26. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  27. package/docs/swagger.md +316 -0
  28. package/examples/controls/1) window/server.js +6 -1
  29. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  30. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  31. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  32. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  33. declarative api/e2e-screenshot-1-name-change.png +0 -0
  34. declarative api/e2e-screenshot-2-toggled.png +0 -0
  35. declarative api/e2e-screenshot-3-final.png +0 -0
  36. declarative api/e2e-screenshot-final.png +0 -0
  37. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  38. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  39. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  40. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  41. package/examples/data-views/01) query-endpoint/server.js +61 -0
  42. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  43. package/labs/website-design/002-pages-storage/check.js +244 -0
  44. package/labs/website-design/002-pages-storage/results.txt +0 -0
  45. package/labs/website-design/003-type-detection/check.js +193 -0
  46. package/labs/website-design/003-type-detection/results.txt +0 -0
  47. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  48. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  49. package/labs/website-design/005-normalize-input/check.js +303 -0
  50. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  51. package/labs/website-design/README.md +34 -0
  52. package/labs/website-design/manifest.json +68 -0
  53. package/labs/website-design/run-all.js +60 -0
  54. package/middleware/json-body.js +126 -0
  55. package/openapi.js +474 -0
  56. package/package.json +11 -8
  57. package/publishers/Publishers.js +6 -5
  58. package/publishers/http-function-publisher.js +135 -126
  59. package/publishers/http-webpage-publisher.js +89 -11
  60. package/publishers/query-publisher.js +116 -0
  61. package/publishers/swagger-publisher.js +203 -0
  62. package/publishers/swagger-ui.js +578 -0
  63. package/resources/adapters/array-adapter.js +143 -0
  64. package/resources/query-resource.js +131 -0
  65. package/serve-factory.js +728 -18
  66. package/server.js +421 -103
  67. package/tests/README.md +23 -1
  68. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  69. package/tests/helpers/playwright-e2e-harness.js +326 -0
  70. package/tests/openapi.test.js +319 -0
  71. package/tests/playwright-smoke.test.js +134 -0
  72. package/tests/publish-enhancements.test.js +673 -0
  73. package/tests/query-publisher.test.js +430 -0
  74. package/tests/quick-json-body-test.js +169 -0
  75. package/tests/serve.test.js +425 -122
  76. package/tests/swagger-publisher.test.js +1076 -0
  77. package/tests/test-runner.js +1 -0
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Swagger UI HTML Page Generator for jsgui3-server.
3
+ *
4
+ * Produces a self-contained HTML page that loads
5
+ * [Swagger UI](https://github.com/swagger-api/swagger-ui) from the
6
+ * [unpkg CDN](https://unpkg.com/) and points it at the server's own
7
+ * `/api/openapi.json` endpoint.
8
+ *
9
+ * ## Zero Dependencies
10
+ *
11
+ * No npm packages are installed. Both the CSS and JavaScript for
12
+ * Swagger UI are loaded at runtime from the CDN, so no build step
13
+ * or bundling is needed.
14
+ *
15
+ * ## Theming
16
+ *
17
+ * Five built-in colour themes are available, switchable at runtime via a
18
+ * dropdown selector in the Swagger UI topbar. Themes are pure CSS variable
19
+ * swaps using the `[data-theme]` selector on `<html>`.
20
+ *
21
+ * | Theme | Description |
22
+ * |------------------|----------------------------------|
23
+ * | `wlilo` | Deep obsidian/leather/gold dark |
24
+ * | `midnight` | Deep blue-purple dark |
25
+ * | `light` | Clean light mode |
26
+ * | `nord` | Nord-inspired muted palette |
27
+ * | `high-contrast` | WCAG AAA high-contrast dark |
28
+ *
29
+ * Selection is persisted in `localStorage` (key: `swagger-theme`).
30
+ * Custom themes can be passed via `options.themes`.
31
+ *
32
+ * ## CSS Variables
33
+ *
34
+ * All themes set these core variables:
35
+ *
36
+ * | Variable | Purpose |
37
+ * |----------------------|--------------------|
38
+ * | `--swagger-bg` | Page background |
39
+ * | `--swagger-surface` | Card / block bg |
40
+ * | `--swagger-border` | Border colour |
41
+ * | `--swagger-text` | Primary text |
42
+ * | `--swagger-muted` | Secondary text |
43
+ * | `--swagger-accent` | Accent / links |
44
+ * | `--swagger-danger` | Required / errors |
45
+ * | `--swagger-success` | Success indicators |
46
+ * | `--swagger-code-bg` | Code block bg |
47
+ * | `--swagger-code-fg` | Code block text |
48
+ * | `--swagger-input-bg` | Input background |
49
+ *
50
+ * ## CDN Version
51
+ *
52
+ * The major version of Swagger UI loaded from CDN is controlled by the
53
+ * module-level constant `SWAGGER_UI_CDN_VERSION` (currently `'5'`).
54
+ * unpkg resolves this to the latest 5.x release automatically.
55
+ *
56
+ * ## Usage
57
+ *
58
+ * This module is used internally by
59
+ * {@link Swagger_Publisher} and does not normally need to be called directly.
60
+ *
61
+ * ```js
62
+ * const { generate_swagger_html } = require('./publishers/swagger-ui');
63
+ * const html = generate_swagger_html({ title: 'My API' });
64
+ * // → Complete HTML page string
65
+ * ```
66
+ *
67
+ * @module publishers/swagger-ui
68
+ * @see {@link module:openapi} — the spec generator that produces the JSON this page renders.
69
+ * @see {@link module:publishers/swagger-publisher} — the publisher that serves this page.
70
+ */
71
+
72
+ 'use strict';
73
+
74
+ /**
75
+ * Major version of Swagger UI to load from unpkg CDN.
76
+ *
77
+ * unpkg resolves `swagger-ui-dist@5` to the latest 5.x.y release.
78
+ * Pin to a specific version (e.g. `'5.17.14'`) if reproducibility
79
+ * is more important than auto-updates.
80
+ *
81
+ * @const {string}
82
+ */
83
+ const SWAGGER_UI_CDN_VERSION = '5';
84
+
85
+ /**
86
+ * Built-in theme presets.
87
+ *
88
+ * Each theme is a map of CSS custom property values used throughout
89
+ * the Swagger UI page. Themes are activated by setting
90
+ * `document.documentElement.dataset.theme` at runtime.
91
+ *
92
+ * @const {Object<string, {label: string, vars: Object<string, string>}>}
93
+ */
94
+ const BUILTIN_THEMES = {
95
+ wlilo: {
96
+ label: 'WLILO Dark',
97
+ vars: {
98
+ '--swagger-bg': '#0f1225',
99
+ '--swagger-surface': '#181c35',
100
+ '--swagger-border': '#2a2f52',
101
+ '--swagger-text': '#e0e0f0',
102
+ '--swagger-muted': '#9aa0c8',
103
+ '--swagger-accent': '#c9a84c',
104
+ '--swagger-danger': '#ff6b81',
105
+ '--swagger-success': '#50fa7b',
106
+ '--swagger-code-bg': '#0d0f1e',
107
+ '--swagger-code-fg': '#c9d1d9',
108
+ '--swagger-input-bg': '#0d0f1e',
109
+ }
110
+ },
111
+ midnight: {
112
+ label: 'Midnight',
113
+ vars: {
114
+ '--swagger-bg': '#0b0e1a',
115
+ '--swagger-surface': '#131729',
116
+ '--swagger-border': '#252a45',
117
+ '--swagger-text': '#d4d8ef',
118
+ '--swagger-muted': '#8b92b8',
119
+ '--swagger-accent': '#4facfe',
120
+ '--swagger-danger': '#ef4444',
121
+ '--swagger-success': '#22c55e',
122
+ '--swagger-code-bg': '#090c16',
123
+ '--swagger-code-fg': '#a5b4d4',
124
+ '--swagger-input-bg': '#090c16',
125
+ }
126
+ },
127
+ light: {
128
+ label: 'Light',
129
+ vars: {
130
+ '--swagger-bg': '#f8f9fa',
131
+ '--swagger-surface': '#ffffff',
132
+ '--swagger-border': '#dee2e6',
133
+ '--swagger-text': '#212529',
134
+ '--swagger-muted': '#6c757d',
135
+ '--swagger-accent': '#0d6efd',
136
+ '--swagger-danger': '#dc3545',
137
+ '--swagger-success': '#198754',
138
+ '--swagger-code-bg': '#f1f3f5',
139
+ '--swagger-code-fg': '#212529',
140
+ '--swagger-input-bg': '#ffffff',
141
+ }
142
+ },
143
+ nord: {
144
+ label: 'Nord',
145
+ vars: {
146
+ '--swagger-bg': '#2e3440',
147
+ '--swagger-surface': '#3b4252',
148
+ '--swagger-border': '#4c566a',
149
+ '--swagger-text': '#eceff4',
150
+ '--swagger-muted': '#d8dee9',
151
+ '--swagger-accent': '#88c0d0',
152
+ '--swagger-danger': '#bf616a',
153
+ '--swagger-success': '#a3be8c',
154
+ '--swagger-code-bg': '#2e3440',
155
+ '--swagger-code-fg': '#e5e9f0',
156
+ '--swagger-input-bg': '#2e3440',
157
+ }
158
+ },
159
+ 'high-contrast': {
160
+ label: 'High Contrast',
161
+ vars: {
162
+ '--swagger-bg': '#000000',
163
+ '--swagger-surface': '#1a1a1a',
164
+ '--swagger-border': '#ffffff',
165
+ '--swagger-text': '#ffffff',
166
+ '--swagger-muted': '#e0e0e0',
167
+ '--swagger-accent': '#ffff00',
168
+ '--swagger-danger': '#ff4444',
169
+ '--swagger-success': '#00ff00',
170
+ '--swagger-code-bg': '#0a0a0a',
171
+ '--swagger-code-fg': '#ffffff',
172
+ '--swagger-input-bg': '#1a1a1a',
173
+ }
174
+ }
175
+ };
176
+
177
+ /**
178
+ * Build CSS blocks for all themes.
179
+ *
180
+ * Each theme generates a `[data-theme="name"]` selector block that sets
181
+ * the CSS custom properties for that theme.
182
+ *
183
+ * @param {Object} themes - Theme definitions (same shape as BUILTIN_THEMES).
184
+ * @returns {string} CSS text.
185
+ * @private
186
+ */
187
+ const build_theme_css = (themes) => {
188
+ const blocks = [];
189
+ for (const [name, theme] of Object.entries(themes)) {
190
+ const vars = Object.entries(theme.vars)
191
+ .map(([k, v]) => ` ${k}: ${v};`)
192
+ .join('\n');
193
+ blocks.push(` [data-theme="${name}"] {\n${vars}\n }`);
194
+ }
195
+ return blocks.join('\n');
196
+ };
197
+
198
+ /**
199
+ * Build the JavaScript for the theme selector widget.
200
+ *
201
+ * Injects a `<select>` into the Swagger UI topbar after it renders.
202
+ * Persists selection to `localStorage`.
203
+ *
204
+ * @param {Object} themes - Theme definitions.
205
+ * @param {string} default_theme - Default theme name.
206
+ * @returns {string} JavaScript source.
207
+ * @private
208
+ */
209
+ const build_theme_js = (themes, default_theme) => {
210
+ const options_arr = Object.entries(themes).map(([name, t]) =>
211
+ `{ value: ${JSON.stringify(name)}, label: ${JSON.stringify(t.label)} }`
212
+ );
213
+ return `
214
+ // ── Theme Selector ──
215
+ (function() {
216
+ var STORAGE_KEY = 'swagger-theme';
217
+ var DEFAULT_THEME = ${JSON.stringify(default_theme)};
218
+ var themes = [${options_arr.join(', ')}];
219
+
220
+ function applyTheme(name) {
221
+ document.documentElement.setAttribute('data-theme', name);
222
+ try { localStorage.setItem(STORAGE_KEY, name); } catch(e) {}
223
+ }
224
+
225
+ // Apply saved or default theme immediately.
226
+ var saved = null;
227
+ try { saved = localStorage.getItem(STORAGE_KEY); } catch(e) {}
228
+ applyTheme(saved || DEFAULT_THEME);
229
+
230
+ // Wait for Swagger UI to render, then inject the selector.
231
+ var attempts = 0;
232
+ var interval = setInterval(function() {
233
+ attempts++;
234
+ var topbar = document.querySelector('.swagger-ui .topbar-wrapper');
235
+ if (!topbar && attempts < 50) return;
236
+ clearInterval(interval);
237
+ if (!topbar) return;
238
+
239
+ var wrapper = document.createElement('div');
240
+ wrapper.className = 'theme-selector';
241
+ wrapper.style.cssText = 'display:flex;align-items:center;gap:8px;margin-left:auto;padding-right:12px;';
242
+
243
+ var label = document.createElement('span');
244
+ label.textContent = 'Theme';
245
+ label.style.cssText = 'font-size:12px;text-transform:uppercase;letter-spacing:0.08em;opacity:0.7;color:var(--swagger-text);';
246
+
247
+ var select = document.createElement('select');
248
+ select.id = 'theme-select';
249
+ select.style.cssText = 'background:var(--swagger-input-bg);color:var(--swagger-text);border:1px solid var(--swagger-border);border-radius:6px;padding:6px 10px;font-size:13px;cursor:pointer;outline:none;min-width:130px;';
250
+ select.setAttribute('aria-label', 'Select theme');
251
+
252
+ themes.forEach(function(t) {
253
+ var opt = document.createElement('option');
254
+ opt.value = t.value;
255
+ opt.textContent = t.label;
256
+ if (t.value === (saved || DEFAULT_THEME)) opt.selected = true;
257
+ select.appendChild(opt);
258
+ });
259
+
260
+ select.addEventListener('change', function() {
261
+ applyTheme(this.value);
262
+ });
263
+
264
+ wrapper.appendChild(label);
265
+ wrapper.appendChild(select);
266
+ topbar.style.display = 'flex';
267
+ topbar.style.alignItems = 'center';
268
+ topbar.appendChild(wrapper);
269
+ }, 100);
270
+ })();`;
271
+ };
272
+
273
+ /**
274
+ * Generate a self-contained Swagger UI HTML page with theme support.
275
+ *
276
+ * The returned string is a complete `<!doctype html>` document that
277
+ * can be served directly as `text/html`. It:
278
+ *
279
+ * 1. Loads Swagger UI CSS + JS from unpkg CDN.
280
+ * 2. Initialises `SwaggerUIBundle` pointed at `spec_url`.
281
+ * 3. Provides 5 built-in colour themes (default: WLILO dark).
282
+ * 4. Includes a runtime theme selector in the topbar.
283
+ * 5. Persists theme choice to `localStorage`.
284
+ * 6. Replaces the default Swagger logo with "jsgui3 API Docs".
285
+ *
286
+ * @param {Object} [options] - Configuration options.
287
+ * @param {string} [options.spec_url='/api/openapi.json'] - URL of the OpenAPI spec.
288
+ * @param {string} [options.title='API Documentation'] - HTML page title.
289
+ * @param {string} [options.default_theme='wlilo'] - Default theme name.
290
+ * @param {Object} [options.themes] - Additional custom themes
291
+ * in the same `{ name: { label, vars } }` format as BUILTIN_THEMES.
292
+ * Custom themes are merged with (and can override) built-in themes.
293
+ * @returns {string} Complete HTML page as a string.
294
+ *
295
+ * @example
296
+ * // Default usage (WLILO dark theme, points at /api/openapi.json):
297
+ * const html = generate_swagger_html();
298
+ *
299
+ * @example
300
+ * // Custom title and light theme default:
301
+ * const html = generate_swagger_html({
302
+ * title: 'My Service API',
303
+ * default_theme: 'light'
304
+ * });
305
+ *
306
+ * @example
307
+ * // Add a custom theme:
308
+ * const html = generate_swagger_html({
309
+ * themes: {
310
+ * ocean: {
311
+ * label: 'Ocean Blue',
312
+ * vars: {
313
+ * '--swagger-bg': '#0a192f',
314
+ * '--swagger-surface': '#112240',
315
+ * '--swagger-border': '#233554',
316
+ * '--swagger-text': '#ccd6f6',
317
+ * '--swagger-muted': '#8892b0',
318
+ * '--swagger-accent': '#64ffda',
319
+ * '--swagger-danger': '#ff6b6b',
320
+ * '--swagger-success': '#64ffda',
321
+ * '--swagger-code-bg': '#0a192f',
322
+ * '--swagger-code-fg': '#a8b2d1',
323
+ * '--swagger-input-bg': '#0a192f',
324
+ * }
325
+ * }
326
+ * }
327
+ * });
328
+ */
329
+ const generate_swagger_html = (options = {}) => {
330
+ const spec_url = options.spec_url || '/api/openapi.json';
331
+ const title = options.title || 'API Documentation';
332
+ const default_theme = options.default_theme || 'wlilo';
333
+
334
+ // Merge built-in themes with optional custom themes.
335
+ const all_themes = { ...BUILTIN_THEMES };
336
+ if (options.themes && typeof options.themes === 'object') {
337
+ Object.assign(all_themes, options.themes);
338
+ }
339
+
340
+ const theme_css = build_theme_css(all_themes);
341
+ const theme_js = build_theme_js(all_themes, default_theme);
342
+
343
+ return `<!doctype html>
344
+ <html lang="en" data-theme="${esc(default_theme)}">
345
+ <head>
346
+ <meta charset="utf-8" />
347
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
348
+ <title>${esc(title)}</title>
349
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@${SWAGGER_UI_CDN_VERSION}/swagger-ui.css" />
350
+ <style>
351
+ /* ── Theme Presets ──────────────────────────────────── */
352
+ ${theme_css}
353
+
354
+ /* ── Shared Layout ──────────────────────────────────── */
355
+ html, body {
356
+ margin: 0;
357
+ padding: 0;
358
+ background: var(--swagger-bg);
359
+ color: var(--swagger-text);
360
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
361
+ Helvetica, Arial, sans-serif;
362
+ transition: background-color 0.3s ease, color 0.3s ease;
363
+ }
364
+
365
+ /* ── Topbar ─────────────────────────────────────────── */
366
+ .swagger-ui .topbar {
367
+ background: var(--swagger-surface);
368
+ border-bottom: 1px solid var(--swagger-border);
369
+ padding: 8px 0;
370
+ transition: background-color 0.3s ease, border-color 0.3s ease;
371
+ }
372
+ .swagger-ui .topbar .download-url-wrapper {
373
+ display: flex;
374
+ align-items: center;
375
+ }
376
+ .swagger-ui .topbar .download-url-wrapper .download-url-button {
377
+ background: var(--swagger-accent);
378
+ border: 1px solid var(--swagger-accent);
379
+ color: #fff;
380
+ }
381
+
382
+ /* ── Branding ───────────────────────────────────────── */
383
+ .swagger-ui .topbar-wrapper img[alt="Swagger UI"] { display: none; }
384
+ .swagger-ui .topbar-wrapper::before {
385
+ content: 'jsgui3 API Docs';
386
+ color: var(--swagger-accent);
387
+ font-size: 1.1rem;
388
+ font-weight: 600;
389
+ letter-spacing: 0.02em;
390
+ }
391
+
392
+ /* ── Info Section ───────────────────────────────────── */
393
+ .swagger-ui .info .title { color: var(--swagger-text); }
394
+ .swagger-ui .info p,
395
+ .swagger-ui .info li,
396
+ .swagger-ui .info table { color: var(--swagger-muted); }
397
+
398
+ /* ── Scheme Container ───────────────────────────────── */
399
+ .swagger-ui .scheme-container {
400
+ background: var(--swagger-surface);
401
+ box-shadow: none;
402
+ border: 1px solid var(--swagger-border);
403
+ }
404
+
405
+ /* ── Operations ─────────────────────────────────────── */
406
+ .swagger-ui .opblock-tag {
407
+ color: var(--swagger-text);
408
+ border-bottom-color: var(--swagger-border);
409
+ }
410
+ .swagger-ui .opblock {
411
+ border-color: var(--swagger-border);
412
+ background: var(--swagger-surface);
413
+ transition: background-color 0.2s ease;
414
+ }
415
+ .swagger-ui .opblock .opblock-summary {
416
+ border-color: var(--swagger-border);
417
+ }
418
+ .swagger-ui .opblock .opblock-summary-description {
419
+ color: var(--swagger-muted);
420
+ }
421
+ .swagger-ui .opblock-description-wrapper p,
422
+ .swagger-ui .opblock-external-docs-wrapper p,
423
+ .swagger-ui .opblock-title_normal p {
424
+ color: var(--swagger-muted);
425
+ }
426
+ .swagger-ui .opblock .opblock-section-header {
427
+ background: rgba(79, 172, 254, 0.06);
428
+ box-shadow: none;
429
+ }
430
+ .swagger-ui .opblock .opblock-section-header h4 {
431
+ color: var(--swagger-text);
432
+ }
433
+
434
+ /* ── Deprecated strike-through + muted ──────────────── */
435
+ .swagger-ui .opblock-deprecated {
436
+ opacity: 0.6;
437
+ border-color: var(--swagger-danger);
438
+ }
439
+ .swagger-ui .opblock-deprecated .opblock-summary-method {
440
+ background: var(--swagger-danger);
441
+ }
442
+
443
+ /* ── Tables ─────────────────────────────────────────── */
444
+ .swagger-ui table thead tr td,
445
+ .swagger-ui table thead tr th {
446
+ color: var(--swagger-text);
447
+ border-bottom-color: var(--swagger-border);
448
+ }
449
+ .swagger-ui .parameter__name,
450
+ .swagger-ui .parameter__type {
451
+ color: var(--swagger-muted);
452
+ }
453
+ .swagger-ui .parameter__name.required::after {
454
+ color: var(--swagger-danger);
455
+ }
456
+
457
+ /* ── Responses ──────────────────────────────────────── */
458
+ .swagger-ui .response-col_status {
459
+ color: var(--swagger-text);
460
+ }
461
+ .swagger-ui .response-col_description__inner p {
462
+ color: var(--swagger-muted);
463
+ }
464
+ .swagger-ui .responses-inner h4,
465
+ .swagger-ui .responses-inner h5 {
466
+ color: var(--swagger-text);
467
+ }
468
+
469
+ /* ── Models ─────────────────────────────────────────── */
470
+ .swagger-ui .model-title { color: var(--swagger-text); }
471
+ .swagger-ui .model { color: var(--swagger-muted); }
472
+
473
+ /* ── Form Elements ──────────────────────────────────── */
474
+ .swagger-ui select {
475
+ background: var(--swagger-input-bg);
476
+ color: var(--swagger-text);
477
+ border-color: var(--swagger-border);
478
+ }
479
+ .swagger-ui input[type=text],
480
+ .swagger-ui textarea {
481
+ background: var(--swagger-input-bg);
482
+ color: var(--swagger-text);
483
+ border-color: var(--swagger-border);
484
+ }
485
+ .swagger-ui .btn {
486
+ color: var(--swagger-text);
487
+ border-color: var(--swagger-border);
488
+ }
489
+ .swagger-ui .btn.execute {
490
+ background: var(--swagger-accent);
491
+ border-color: var(--swagger-accent);
492
+ color: #fff;
493
+ }
494
+ .swagger-ui .btn.cancel {
495
+ color: var(--swagger-danger);
496
+ border-color: var(--swagger-danger);
497
+ }
498
+
499
+ /* ── Code Blocks ────────────────────────────────────── */
500
+ .swagger-ui .highlight-code .microlight {
501
+ background: var(--swagger-code-bg);
502
+ color: var(--swagger-code-fg);
503
+ border: 1px solid var(--swagger-border);
504
+ border-radius: 6px;
505
+ }
506
+ .swagger-ui .response .renderedMarkdown code {
507
+ background: var(--swagger-code-bg);
508
+ color: var(--swagger-code-fg);
509
+ }
510
+
511
+ /* ── Layout ─────────────────────────────────────────── */
512
+ .swagger-ui .wrapper {
513
+ max-width: 1200px;
514
+ }
515
+
516
+ /* ── Smooth transitions for theme switching ──────────── */
517
+ .swagger-ui .opblock,
518
+ .swagger-ui .scheme-container,
519
+ .swagger-ui .topbar,
520
+ .swagger-ui .model-container {
521
+ transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
522
+ }
523
+
524
+ /* ── Theme Selector widget ──────────────────────────── */
525
+ .theme-selector select:hover {
526
+ border-color: var(--swagger-accent);
527
+ }
528
+ .theme-selector select:focus {
529
+ border-color: var(--swagger-accent);
530
+ box-shadow: 0 0 0 2px rgba(79, 172, 254, 0.25);
531
+ }
532
+ </style>
533
+ </head>
534
+ <body>
535
+ <div id="swagger-ui"></div>
536
+ <script src="https://unpkg.com/swagger-ui-dist@${SWAGGER_UI_CDN_VERSION}/swagger-ui-bundle.js" crossorigin></script>
537
+ <script>
538
+ window.onload = function () {
539
+ SwaggerUIBundle({
540
+ url: ${JSON.stringify(spec_url)},
541
+ dom_id: '#swagger-ui',
542
+ deepLinking: true,
543
+ presets: [
544
+ SwaggerUIBundle.presets.apis,
545
+ SwaggerUIBundle.SwaggerUIStandalonePreset
546
+ ],
547
+ layout: 'BaseLayout',
548
+ defaultModelsExpandDepth: 1,
549
+ defaultModelExpandDepth: 2,
550
+ docExpansion: 'list',
551
+ filter: true,
552
+ showExtensions: true,
553
+ showCommonExtensions: true
554
+ });
555
+ ${theme_js}
556
+ };
557
+ </script>
558
+ </body>
559
+ </html>`;
560
+ };
561
+
562
+ /**
563
+ * Escape a string for safe inclusion in HTML attributes and text content.
564
+ *
565
+ * Handles the four characters that can break HTML context:
566
+ * `&`, `<`, `>`, and `"`.
567
+ *
568
+ * @param {string} str - The string to escape.
569
+ * @returns {string} HTML-safe string.
570
+ * @private
571
+ */
572
+ const esc = (str) => String(str)
573
+ .replace(/&/g, '&amp;')
574
+ .replace(/</g, '&lt;')
575
+ .replace(/>/g, '&gt;')
576
+ .replace(/"/g, '&quot;');
577
+
578
+ module.exports = { generate_swagger_html, BUILTIN_THEMES };