docula 2.0.0 → 2.2.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": "2.0.0",
3
+ "version": "2.2.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": "^22.18.0 || >=24.0.0"
20
+ "node": "^22.19.0 || >=24.0.0"
21
21
  },
22
22
  "license": "MIT",
23
23
  "keywords": [
@@ -40,32 +40,34 @@
40
40
  "docula": "./bin/docula.js"
41
41
  },
42
42
  "dependencies": {
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",
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.5",
50
- "hashery": "^2.0.0",
49
+ "ecto": "^4.8.7",
50
+ "hashery": "^3.0.0",
51
+ "ipaddr.js": "2.4.0",
51
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
56
  "writr": "^6.1.2"
55
57
  },
56
58
  "devDependencies": {
57
- "@biomejs/biome": "^2.4.15",
59
+ "@biomejs/biome": "^2.4.16",
58
60
  "@playwright/test": "^1.60.0",
59
- "@types/node": "^24.12.4",
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.6",
64
+ "@vitest/coverage-v8": "^4.1.8",
63
65
  "dotenv": "^17.4.2",
64
66
  "rimraf": "^6.1.3",
65
- "tsdown": "^0.22.0",
66
- "tsx": "^4.21.0",
67
+ "tsdown": "^0.22.2",
68
+ "tsx": "^4.22.4",
67
69
  "typescript": "^6.0.3",
68
- "vitest": "^4.1.6"
70
+ "vitest": "^4.1.8"
69
71
  },
70
72
  "files": [
71
73
  "dist",
@@ -1,7 +1,7 @@
1
1
  {{#if googleTagManager}}
2
2
  {{#unless isGtag}}
3
3
  <!-- Google Tag Manager (noscript) -->
4
- <noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{googleTagManager}}"
4
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{googleTagManager}}{{{googleTagManagerParams}}}"
5
5
  height="0" width="0" style="display:none !important;visibility:hidden !important"></iframe></noscript>
6
6
  <!-- End Google Tag Manager (noscript) -->
7
7
  {{/unless}}
@@ -8,7 +8,7 @@
8
8
  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
9
9
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
10
10
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
11
- 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
11
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl+'{{{googleTagManagerParams}}}';f.parentNode.insertBefore(j,f);
12
12
  })(window,document,'script','dataLayer','{{googleTagManager}}');</script>
13
13
  <!-- End Google Tag Manager -->
14
14
  {{/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 {
@@ -1,7 +1,7 @@
1
1
  {{#if googleTagManager}}
2
2
  {{#unless isGtag}}
3
3
  <!-- Google Tag Manager (noscript) -->
4
- <noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{googleTagManager}}"
4
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{googleTagManager}}{{{googleTagManagerParams}}}"
5
5
  height="0" width="0" style="display:none !important;visibility:hidden !important"></iframe></noscript>
6
6
  <!-- End Google Tag Manager (noscript) -->
7
7
  {{/unless}}
@@ -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 }}
@@ -8,7 +8,7 @@
8
8
  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
9
9
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
10
10
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
11
- 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
11
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl+'{{{googleTagManagerParams}}}';f.parentNode.insertBefore(j,f);
12
12
  })(window,document,'script','dataLayer','{{googleTagManager}}');</script>
13
13
  <!-- End Google Tag Manager -->
14
14
  {{/if}}
@@ -265,23 +265,34 @@
265
265
  });
266
266
  }
267
267
 
268
- // Active header nav link highlighting
269
- const navLinks = document.querySelectorAll('.header-bottom__item');
270
- navLinks.forEach((link) => {
271
- const href = link.getAttribute('href');
272
- if (currentPath.startsWith(href)) {
273
- link.classList.add('header-bottom__item--active');
274
- }
275
- });
276
-
277
- // Active mobile nav link highlighting
278
- const mobileNavLinks = document.querySelectorAll('.mobile-nav__item');
279
- mobileNavLinks.forEach((link) => {
280
- const href = link.getAttribute('href');
281
- if (currentPath.startsWith(href)) {
282
- link.classList.add('mobile-nav__item--active');
283
- }
284
- });
268
+ // Active nav link highlighting (header + mobile). Highlight only the
269
+ // single most-specific link: the one whose path is the longest prefix of
270
+ // the current path. A plain startsWith() check lights up every link whose
271
+ // path merely prefixes the current one, so when the site is embedded under
272
+ // a base path (e.g. served at /developer with docs at that root) the
273
+ // Documentation link — whose path is a prefix of every page — would stay
274
+ // highlighted everywhere. Trailing slashes are normalized so matches stop
275
+ // at a path boundary. We read the anchor's resolved origin/pathname so
276
+ // absolute, relative, and same-origin hrefs all work, while cross-origin
277
+ // links and non-anchor items (e.g. the mobile logout button) are skipped.
278
+ const withTrailingSlash = (p) => (p.endsWith('/') ? p : p + '/');
279
+ const here = withTrailingSlash(currentPath);
280
+ const markActiveNav = (selector, activeClass) => {
281
+ let bestLink = null;
282
+ let bestLength = -1;
283
+ document.querySelectorAll(selector).forEach((link) => {
284
+ // Only same-origin anchors can match the current pathname.
285
+ if (!link.href || link.origin !== window.location.origin) return;
286
+ const base = withTrailingSlash(link.pathname);
287
+ if (here.startsWith(base) && base.length > bestLength) {
288
+ bestLink = link;
289
+ bestLength = base.length;
290
+ }
291
+ });
292
+ if (bestLink) bestLink.classList.add(activeClass);
293
+ };
294
+ markActiveNav('.header-bottom__item', 'header-bottom__item--active');
295
+ markActiveNav('.mobile-nav__item', 'mobile-nav__item--active');
285
296
 
286
297
  // TOC extraction: move the inline TOC into the right sidebar and hide the original
287
298
  const tocHeading = document.getElementById('table-of-contents');
@@ -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}}