docula 1.14.0 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "1.14.0",
3
+ "version": "2.1.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"
20
+ "node": "^22.19.0 || >=24.0.0"
21
21
  },
22
22
  "license": "MIT",
23
23
  "keywords": [
@@ -40,33 +40,34 @@
40
40
  "docula": "./bin/docula.js"
41
41
  },
42
42
  "dependencies": {
43
- "@ai-sdk/anthropic": "^3.0.69",
44
- "@ai-sdk/google": "^3.0.63",
45
- "@ai-sdk/openai": "^3.0.53",
46
- "@cacheable/net": "^2.0.7",
47
- "ai": "^6.0.164",
43
+ "@ai-sdk/anthropic": "^3.0.83",
44
+ "@ai-sdk/google": "^3.0.81",
45
+ "@ai-sdk/openai": "^3.0.70",
46
+ "@cacheable/net": "^2.0.8",
47
+ "ai": "^6.0.202",
48
48
  "colorette": "^2.0.20",
49
- "ecto": "^4.8.4",
50
- "hashery": "^2.0.0",
51
- "jiti": "^2.6.1",
49
+ "ecto": "^4.8.7",
50
+ "hashery": "^3.0.0",
51
+ "ipaddr.js": "2.4.0",
52
+ "jiti": "^2.7.0",
52
53
  "serve-handler": "^6.1.7",
54
+ "undici": "8.4.1",
53
55
  "update-notifier": "^7.3.1",
54
- "writr": "^6.1.1"
56
+ "writr": "^6.1.2"
55
57
  },
56
58
  "devDependencies": {
57
- "@biomejs/biome": "^2.4.12",
58
- "@playwright/test": "^1.59.1",
59
- "@types/node": "^25.6.0",
59
+ "@biomejs/biome": "^2.4.16",
60
+ "@playwright/test": "^1.60.0",
61
+ "@types/node": "^24.13.2",
60
62
  "@types/serve-handler": "^6.1.4",
61
63
  "@types/update-notifier": "^6.0.8",
62
- "@vitest/coverage-v8": "^4.1.4",
64
+ "@vitest/coverage-v8": "^4.1.8",
63
65
  "dotenv": "^17.4.2",
64
- "postject": "1.0.0-alpha.6",
65
66
  "rimraf": "^6.1.3",
66
- "tsdown": "^0.21.9",
67
- "tsx": "^4.21.0",
68
- "typescript": "^6.0.2",
69
- "vitest": "^4.1.4"
67
+ "tsdown": "^0.22.2",
68
+ "tsx": "^4.22.4",
69
+ "typescript": "^6.0.3",
70
+ "vitest": "^4.1.8"
70
71
  },
71
72
  "files": [
72
73
  "dist",
@@ -76,7 +77,7 @@
76
77
  "scripts": {
77
78
  "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/dist",
78
79
  "build": "pnpm generate-init-file && tsdown",
79
- "build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts && tsx scripts/build-sea.ts",
80
+ "build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts",
80
81
  "generate-embedded-templates": "tsx scripts/generate-embedded-templates.ts",
81
82
  "lint": "biome check --write --error-on-warnings",
82
83
  "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="/favicon.ico" />
34
+ {{#if faviconUrl}}<link rel="icon" href="{{faviconUrl}}" />{{/if}}
@@ -221,16 +221,27 @@ body {
221
221
  }
222
222
 
223
223
  .search-button {
224
- border: none;
225
- border-radius: 4px;
224
+ border: 1px solid var(--border);
225
+ border-radius: 6px;
226
226
  padding: 0px 10px;
227
- height: 30px;
228
- width: 150px;
227
+ height: 32px;
228
+ width: 200px;
229
229
  display: flex;
230
230
  align-items: center;
231
- gap: 6px;
231
+ gap: 8px;
232
232
  color: var(--muted-fg);
233
233
  background: var(--surface);
234
+ cursor: pointer;
235
+ transition: border-color 0.15s ease, color 0.15s ease;
236
+ }
237
+
238
+ .search-button:hover {
239
+ border-color: var(--border-hover);
240
+ color: var(--fg);
241
+ }
242
+
243
+ .search-button svg {
244
+ flex-shrink: 0;
234
245
  }
235
246
 
236
247
  .search-button__text {
@@ -241,9 +252,254 @@ body {
241
252
  }
242
253
 
243
254
  .search-button__shortcut {
244
- font-size: 11px;
255
+ font-size: 12px;
245
256
  font-weight: 500;
246
- opacity: 0.6;
257
+ padding: 1px 6px;
258
+ border: 1px solid var(--border);
259
+ border-radius: 4px;
260
+ opacity: 0.7;
261
+ }
262
+
263
+ /* Search modal */
264
+ body.search-open {
265
+ overflow: hidden;
266
+ }
267
+
268
+ .search-modal {
269
+ position: fixed;
270
+ inset: 0;
271
+ z-index: 200;
272
+ display: flex;
273
+ align-items: flex-start;
274
+ justify-content: center;
275
+ padding: 12vh 16px 16px;
276
+ }
277
+
278
+ .search-modal[hidden] {
279
+ display: none;
280
+ }
281
+
282
+ .search-modal__backdrop {
283
+ position: absolute;
284
+ inset: 0;
285
+ background: rgba(0, 0, 0, 0.6);
286
+ backdrop-filter: blur(2px);
287
+ }
288
+
289
+ .search-modal__panel {
290
+ position: relative;
291
+ display: flex;
292
+ flex-direction: column;
293
+ width: min(680px, 100%);
294
+ max-height: 76vh;
295
+ background: var(--bg);
296
+ border: 1px solid var(--border-strong);
297
+ border-radius: 12px;
298
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
299
+ overflow: hidden;
300
+ }
301
+
302
+ .search-modal__panel:focus-within {
303
+ border-color: var(--link);
304
+ }
305
+
306
+ .search-modal__header {
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 10px;
310
+ padding: 14px 16px;
311
+ border-bottom: 1px solid var(--border);
312
+ }
313
+
314
+ .search-modal__icon {
315
+ color: var(--muted-fg);
316
+ flex-shrink: 0;
317
+ }
318
+
319
+ .search-modal__input {
320
+ flex: 1;
321
+ min-width: 0;
322
+ border: none;
323
+ background: transparent;
324
+ color: var(--fg);
325
+ font-size: 16px;
326
+ font-family: inherit;
327
+ outline: none;
328
+ }
329
+
330
+ .search-modal__input::placeholder {
331
+ color: var(--muted-fg);
332
+ }
333
+
334
+ .search-modal__input::-webkit-search-cancel-button {
335
+ display: none;
336
+ }
337
+
338
+ .search-modal__clear {
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: center;
342
+ border: none;
343
+ background: transparent;
344
+ color: var(--muted-fg);
345
+ cursor: pointer;
346
+ padding: 2px;
347
+ border-radius: 4px;
348
+ }
349
+
350
+ .search-modal__clear:hover {
351
+ color: var(--fg);
352
+ background: var(--surface-hover);
353
+ }
354
+
355
+ .search-modal__clear[hidden] {
356
+ display: none;
357
+ }
358
+
359
+ .search-modal__body {
360
+ flex: 1;
361
+ overflow-y: auto;
362
+ padding: 8px;
363
+ }
364
+
365
+ .search-results {
366
+ list-style: none;
367
+ margin: 0;
368
+ padding: 0;
369
+ display: flex;
370
+ flex-direction: column;
371
+ gap: 4px;
372
+ }
373
+
374
+ .search-results[hidden] {
375
+ display: none;
376
+ }
377
+
378
+ .search-result__link {
379
+ display: flex;
380
+ align-items: flex-start;
381
+ gap: 12px;
382
+ padding: 10px 12px;
383
+ border-radius: 8px;
384
+ border: 1px solid transparent;
385
+ color: var(--fg);
386
+ text-decoration: none;
387
+ }
388
+
389
+ .search-result--active .search-result__link {
390
+ background: var(--surface-hover);
391
+ border-color: var(--border-strong);
392
+ }
393
+
394
+ .search-result__icon {
395
+ display: flex;
396
+ align-items: center;
397
+ color: var(--muted-fg);
398
+ margin-top: 2px;
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ .search-result--active .search-result__icon {
403
+ color: var(--link);
404
+ }
405
+
406
+ .search-result__content {
407
+ min-width: 0;
408
+ flex: 1;
409
+ }
410
+
411
+ .search-result__breadcrumb {
412
+ font-size: 12px;
413
+ color: var(--muted-fg);
414
+ margin-bottom: 2px;
415
+ white-space: nowrap;
416
+ overflow: hidden;
417
+ text-overflow: ellipsis;
418
+ }
419
+
420
+ .search-result__title {
421
+ font-size: 15px;
422
+ font-weight: 600;
423
+ line-height: 1.3;
424
+ }
425
+
426
+ .search-result__text {
427
+ font-size: 13px;
428
+ color: var(--muted-fg);
429
+ margin-top: 3px;
430
+ display: -webkit-box;
431
+ -webkit-line-clamp: 2;
432
+ -webkit-box-orient: vertical;
433
+ overflow: hidden;
434
+ }
435
+
436
+ .search-result__mark {
437
+ background: transparent;
438
+ color: var(--link);
439
+ font-weight: 700;
440
+ padding: 0;
441
+ }
442
+
443
+ .search-modal__empty,
444
+ .search-modal__initial {
445
+ padding: 28px 16px;
446
+ text-align: center;
447
+ color: var(--muted-fg);
448
+ font-size: 14px;
449
+ }
450
+
451
+ .search-modal__empty[hidden],
452
+ .search-modal__initial[hidden] {
453
+ display: none;
454
+ }
455
+
456
+ .search-modal__footer {
457
+ display: flex;
458
+ flex-wrap: wrap;
459
+ gap: 16px;
460
+ padding: 10px 16px;
461
+ border-top: 1px solid var(--border);
462
+ font-size: 12px;
463
+ color: var(--muted-fg);
464
+ }
465
+
466
+ .search-modal__hint {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 4px;
470
+ }
471
+
472
+ .search-modal__kbd {
473
+ display: inline-flex;
474
+ align-items: center;
475
+ justify-content: center;
476
+ min-width: 18px;
477
+ height: 18px;
478
+ padding: 0 4px;
479
+ border: 1px solid var(--border);
480
+ border-radius: 4px;
481
+ background: var(--surface);
482
+ font-size: 11px;
483
+ font-family: inherit;
484
+ }
485
+
486
+ @media (max-width: 720px) {
487
+ .search-button {
488
+ width: auto;
489
+ }
490
+
491
+ .search-button__text,
492
+ .search-button__shortcut {
493
+ display: none;
494
+ }
495
+
496
+ .search-modal {
497
+ padding: 8vh 10px 10px;
498
+ }
499
+
500
+ .search-modal__footer {
501
+ display: none;
502
+ }
247
503
  }
248
504
 
249
505
  .content {
@@ -398,7 +654,7 @@ body {
398
654
  .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; }
399
655
  pre:hover .copy-code-btn { opacity: 1; }
400
656
  .copy-code-btn:hover { color: var(--fg); }
401
- .article__main img, .changelog-entry-body img { cursor: zoom-in; }
657
+ .article__main img:not(a img), .changelog-entry-body img:not(a img) { cursor: zoom-in; }
402
658
  .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; }
403
659
  .lightbox-overlay--visible { display: flex !important; }
404
660
  .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; }
@@ -40,6 +40,13 @@
40
40
  {{/if}}
41
41
  </nav>
42
42
  <div class="header-actions">
43
+ {{#if enableSearch}}
44
+ <button class="search-button" id="search-button" type="button" aria-label="Search" aria-keyshortcuts="Meta+K Control+K">
45
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
46
+ <span class="search-button__text">Search</span>
47
+ <span class="search-button__shortcut">⌘K</span>
48
+ </button>
49
+ {{/if}}
43
50
  {{#if cookieAuth.loginUrl}}
44
51
  <div class="cookie-auth">
45
52
  <a href="{{cookieAuth.loginUrl}}" class="cookie-auth-btn" id="cookie-auth-login">
@@ -119,3 +126,4 @@
119
126
  <div class="mobile-sidebar__docs" id="mobile-sidebar-docs"></div>
120
127
  </aside>
121
128
  <div class="sidebar-backdrop" id="sidebar-backdrop"></div>
129
+ {{> search }}
@@ -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="{{baseUrl}}/favicon.ico">
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?:\/\//, '');
@@ -236,10 +236,6 @@
236
236
  storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
237
237
  } catch (e) { storedSectionState = {}; }
238
238
 
239
- const activeSidebarLink = document.querySelector('.nav-sidebar__item--active');
240
- const activeSection = activeSidebarLink ? activeSidebarLink.closest('.nav-sidebar__section--collapsible') : null;
241
- const defaultOpenSection = activeSection || collapsibleSections[0];
242
-
243
239
  const setSectionOpen = (section, toggle, open) => {
244
240
  toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
245
241
  section.classList.toggle('nav-sidebar__section--collapsed', !open);
@@ -255,7 +251,7 @@
255
251
 
256
252
  const key = 'section-' + idx;
257
253
  const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
258
- const isOpen = hasStored ? !!storedSectionState[key] : section === defaultOpenSection;
254
+ const isOpen = hasStored ? !!storedSectionState[key] : true;
259
255
  setSectionOpen(section, toggle, isOpen);
260
256
 
261
257
  toggle.addEventListener('click', () => {
@@ -324,6 +320,7 @@
324
320
  document.body.appendChild(lightboxOverlay);
325
321
 
326
322
  document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
323
+ if (img.closest('a')) return;
327
324
  img.addEventListener('click', function() {
328
325
  lightboxImg.src = img.src;
329
326
  lightboxOverlay.classList.add('lightbox-overlay--visible');
@@ -0,0 +1,46 @@
1
+ {{#if enableSearch}}
2
+ <div class="search-modal" id="search-modal" hidden>
3
+ <div class="search-modal__backdrop" data-search-close></div>
4
+ <div class="search-modal__panel" role="dialog" aria-modal="true" aria-label="Search">
5
+ <div class="search-modal__header">
6
+ <svg class="search-modal__icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
7
+ <input
8
+ type="search"
9
+ id="search-input"
10
+ class="search-modal__input"
11
+ placeholder="Search"
12
+ autocomplete="off"
13
+ autocorrect="off"
14
+ autocapitalize="off"
15
+ spellcheck="false"
16
+ aria-label="Search"
17
+ aria-controls="search-results"
18
+ aria-activedescendant=""
19
+ role="combobox"
20
+ aria-expanded="true"
21
+ />
22
+ <button class="search-modal__clear" id="search-clear" type="button" aria-label="Clear search" hidden>
23
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
24
+ </button>
25
+ </div>
26
+ <div class="search-modal__body">
27
+ <ul class="search-results" id="search-results" role="listbox" aria-label="Search results"></ul>
28
+ <div class="search-modal__empty" id="search-empty" hidden>
29
+ No results for "<span class="search-modal__empty-query" id="search-empty-query"></span>"
30
+ </div>
31
+ <div class="search-modal__initial" id="search-initial">
32
+ Type to search the documentation.
33
+ </div>
34
+ </div>
35
+ <div class="search-modal__footer">
36
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↑</kbd><kbd class="search-modal__kbd">↓</kbd> to navigate</span>
37
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↵</kbd> to select</span>
38
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">esc</kbd> to close</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <script>
43
+ window.__doculaSearch = { indexUrl: "{{baseUrl}}/search-index.json" };
44
+ </script>
45
+ <script src="{{baseUrl}}/js/search.js" defer></script>
46
+ {{/if}}