@chassis-ui/docs 0.2.0 → 0.3.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.
Files changed (74) hide show
  1. package/README.md +16 -16
  2. package/index.ts +6 -1
  3. package/package.json +20 -8
  4. package/src/components/DocsSidebar.astro +63 -23
  5. package/src/components/FeatureCard.astro +3 -3
  6. package/src/components/TableOfContents.astro +8 -7
  7. package/src/components/ThemeToggler.astro +15 -36
  8. package/src/components/shortcodes/Blockquote.astro +1 -1
  9. package/src/components/shortcodes/CSSOnly.astro +6 -2
  10. package/src/components/shortcodes/Callout.astro +6 -2
  11. package/src/components/shortcodes/Code.astro +289 -0
  12. package/src/components/shortcodes/DeprecatedIn.astro +5 -2
  13. package/src/components/shortcodes/Example.astro +132 -0
  14. package/src/components/shortcodes/Icon.astro +57 -0
  15. package/src/components/shortcodes/InFigma.astro +7 -2
  16. package/src/components/shortcodes/JsDocs.astro +64 -0
  17. package/src/components/shortcodes/Placeholder.astro +28 -0
  18. package/src/components/shortcodes/ResizableExample.astro +126 -0
  19. package/src/components/shortcodes/ScssDocs.astro +108 -0
  20. package/src/components/shortcodes/ScssDocsSimple.astro +66 -0
  21. package/src/js/color-modes.js +3 -3
  22. package/src/js/example-mode.js +75 -0
  23. package/src/js/search.js +501 -39
  24. package/src/js/theme-color.js +1 -1
  25. package/src/layouts/DocsLayout.astro +11 -14
  26. package/src/layouts/RedirectLayout.astro +1 -0
  27. package/src/layouts/footer/Scripts.astro +14 -9
  28. package/src/layouts/head/Analytics.astro +12 -6
  29. package/src/layouts/head/Favicons.astro +0 -2
  30. package/src/layouts/head/Head.astro +2 -7
  31. package/src/layouts/head/Stylesheet.astro +18 -11
  32. package/src/layouts/header/Header.astro +2 -5
  33. package/src/layouts/header/Navigation.astro +31 -33
  34. package/src/layouts/header/SearchDialog.astro +74 -0
  35. package/src/layouts/header/SearchTrigger.astro +41 -0
  36. package/src/libs/highlight.ts +91 -0
  37. package/src/libs/icon.ts +75 -0
  38. package/src/libs/image.ts +12 -1
  39. package/src/libs/layout.ts +18 -0
  40. package/src/libs/markdown.ts +128 -0
  41. package/src/libs/placeholder.ts +250 -0
  42. package/src/libs/rehype.ts +0 -17
  43. package/src/libs/shortcodes.ts +129 -0
  44. package/src/libs/site.ts +95 -0
  45. package/src/libs/toc.ts +40 -6
  46. package/src/libs/utils.ts +34 -82
  47. package/src/libs/vite.ts +45 -0
  48. package/src/scss/_anchor.scss +20 -16
  49. package/src/scss/_callouts.scss +29 -28
  50. package/src/scss/_content.scss +72 -103
  51. package/src/scss/_footer.scss +18 -19
  52. package/src/scss/_layout.scss +56 -63
  53. package/src/scss/_navbar.scss +19 -123
  54. package/src/scss/_placeholder-img.scss +11 -7
  55. package/src/scss/_scrolling.scss +14 -12
  56. package/src/scss/_search.scss +418 -0
  57. package/src/scss/_shiki.scss +120 -0
  58. package/src/scss/_sidebar.scss +137 -132
  59. package/src/scss/_skippy.scss +6 -4
  60. package/src/scss/_snippets.scss +181 -0
  61. package/src/scss/_toc.scss +97 -93
  62. package/src/scss/_vars.scss +89 -0
  63. package/src/scss/main.scss +75 -80
  64. package/src/js/icon-loader.js +0 -229
  65. package/src/libs/chassis.ts +0 -38
  66. package/src/scss/_brand.scss +0 -59
  67. package/src/scss/_buttons.scss +0 -33
  68. package/src/scss/_clipboard-js.scss +0 -63
  69. package/src/scss/_code.scss +0 -116
  70. package/src/scss/_colors.scss +0 -140
  71. package/src/scss/_docsearch.scss +0 -212
  72. package/src/scss/_settings.scss +0 -29
  73. package/src/scss/_syntax.scss +0 -160
  74. package/src/scss/_variables.scss +0 -73
package/src/js/search.js CHANGED
@@ -1,53 +1,515 @@
1
- // NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT
2
- // IT'S ALL JUST JUNK FOR OUR DOCS!
3
- // ++++++++++++++++++++++++++++++++++++++++++
4
-
5
1
  /*!
6
- * JavaScript for Chassis's docs (https://chassis-ui.com/)
7
- * Copyright 2024-2025 The Chassis Authors
2
+ * JavaScript for Chassis docs search (https://chassis-ui.com/)
3
+ * Copyright 2024-2026 The Chassis Authors
8
4
  * Licensed under the Creative Commons Attribution 3.0 Unported License.
9
5
  * For details, see https://creativecommons.org/licenses/by/3.0/.
10
6
  */
11
7
 
12
- import docsearch from '@docsearch/js'
13
- ;(() => {
14
- // These values will be replaced by Astro's Vite plugin
15
- const CONFIG = {
16
- apiKey: '__API_KEY__',
17
- indexName: '__INDEX_NAME__',
18
- appId: '__APP_ID__'
8
+ // Custom Pagefind integration. We use `@pagefind/component-ui` only for its
9
+ // instance manager / search engine; the trigger, dialog, input, and results
10
+ // list are all built from Chassis primitives. No `<pagefind-*>` element is
11
+ // ever rendered. Dialog open/close is delegated to the Chassis data-API via
12
+ // the trigger and dismiss buttons already in the markup, so no Dialog import
13
+ // is needed here.
14
+
15
+ import { getInstanceManager } from '@pagefind/component-ui'
16
+
17
+ const DIALOG_SELECTOR = '#cxdSearchDialog'
18
+ const SUB_RESULTS_LIMIT = 3
19
+ // How long a search may take before we show the loading skeleton. Most queries
20
+ // resolve faster, so the previous results stay on-screen instead of flashing.
21
+ const LOADING_SKELETON_DELAY_MS = 200
22
+ const RECENT_STORAGE_KEY = 'cxd:search:recent'
23
+ const RECENT_LIMIT = 5
24
+
25
+ const instance = getInstanceManager().getInstance('default')
26
+
27
+ // ─── Recent visits ────────────────────────────────────────────────────────────
28
+ // Opt-out via Do Not Track; silently no-op when storage is unavailable.
29
+
30
+ const isDoNotTrackEnabled = () => {
31
+ if (typeof navigator === 'undefined') return false
32
+ return (
33
+ navigator.doNotTrack === '1' || globalThis.doNotTrack === '1' || navigator.msDoNotTrack === '1'
34
+ )
35
+ }
36
+
37
+ const getRecentVisits = () => {
38
+ if (isDoNotTrackEnabled()) return []
39
+ try {
40
+ const raw = localStorage.getItem(RECENT_STORAGE_KEY)
41
+ const parsed = raw ? JSON.parse(raw) : []
42
+ return Array.isArray(parsed) ? parsed.filter((item) => item && item.url) : []
43
+ } catch {
44
+ return []
19
45
  }
46
+ }
20
47
 
21
- const searchElement = document.getElementById('docsearch')
48
+ const saveRecentVisit = (visit) => {
49
+ if (isDoNotTrackEnabled() || !visit?.url) return
50
+ try {
51
+ const existing = getRecentVisits().filter((item) => item.url !== visit.url)
52
+ const next = [visit, ...existing].slice(0, RECENT_LIMIT)
53
+ localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(next))
54
+ } catch {
55
+ // Storage is best-effort.
56
+ }
57
+ }
22
58
 
23
- if (!searchElement) {
24
- return
59
+ const clearRecentVisits = () => {
60
+ try {
61
+ localStorage.removeItem(RECENT_STORAGE_KEY)
62
+ } catch {
63
+ // Ignore.
64
+ }
65
+ }
66
+
67
+ // ─── HTML helpers ─────────────────────────────────────────────────────────────
68
+
69
+ const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
70
+ const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => HTML_ESCAPES[char])
71
+
72
+ // Single template for every clickable row: top-level results, sub-results, and
73
+ // recently visited items. `title` and `excerpt` are inlined as-is so callers
74
+ // can either escape them (recents, plain text) or pass Pagefind's pre-marked HTML.
75
+ const renderItem = ({ url, title, excerpt = '', icon, sub = false }) => `
76
+ <a class="cxd-search-item${sub ? ' cxd-search-subitem' : ''}" href="${escapeHtml(url)}">
77
+ <svg class="icon cxd-search-item-icon" width="16" height="16" aria-hidden="true">
78
+ <use href="#${icon}"></use>
79
+ </svg>
80
+ <div class="cxd-search-item-body">
81
+ <div class="cxd-search-item-title">${title}</div>
82
+ ${excerpt ? `<div class="cxd-search-item-excerpt">${excerpt}</div>` : ''}
83
+ </div>
84
+ </a>
85
+ `
86
+
87
+ // ─── Custom elements ──────────────────────────────────────────────────────────
88
+
89
+ class CxdSearchInput extends HTMLElement {
90
+ connectedCallback() {
91
+ this.input = this.querySelector('input')
92
+ if (!this.input) return
93
+
94
+ this.inputEl = this.input
95
+ instance.registerInput(this, { keyboardNavigation: true })
96
+
97
+ this._onInput = this._onInput.bind(this)
98
+ this._onKeydown = this._onKeydown.bind(this)
99
+ this.input.addEventListener('input', this._onInput)
100
+ this.input.addEventListener('keydown', this._onKeydown)
101
+
102
+ this.input.addEventListener(
103
+ 'focus',
104
+ () => {
105
+ instance.triggerLoad()
106
+ },
107
+ { once: true }
108
+ )
109
+ }
110
+
111
+ disconnectedCallback() {
112
+ this.input?.removeEventListener('input', this._onInput)
113
+ this.input?.removeEventListener('keydown', this._onKeydown)
114
+ }
115
+
116
+ _onInput(event) {
117
+ instance.triggerSearch(event.target.value)
118
+ }
119
+
120
+ _onKeydown(event) {
121
+ // ArrowDown jumps focus to the first link in the next results component.
122
+ if (event.key === 'ArrowDown' && instance.focusNextResults(this)) {
123
+ event.preventDefault()
124
+ return
125
+ }
126
+
127
+ // Enter follows the first result link.
128
+ // Always preventDefault so the form never implicit-submits.
129
+ if (event.key === 'Enter') {
130
+ event.preventDefault()
131
+ const dialogEl = this.closest('dialog')
132
+ dialogEl?.querySelector('.cxd-search-item')?.click()
133
+ }
25
134
  }
26
135
 
27
- docsearch({
28
- apiKey: CONFIG.apiKey,
29
- indexName: CONFIG.indexName,
30
- appId: CONFIG.appId,
31
- container: searchElement,
32
- transformItems(items) {
33
- return items.map((item) => {
34
- const liveUrl = 'https://chassis-ui.com/'
35
-
36
- item.url = window.location.origin.startsWith(liveUrl)
37
- ? // On production, return the result as is
38
- item.url
39
- : // On development or Vercel, replace `item.url` with a trailing slash,
40
- // so that the result link is relative to the server root
41
- item.url.replace(liveUrl, '/')
42
-
43
- // Prevent jumping to first header
44
- if (item.anchor === 'content') {
45
- item.url = item.url.replace(/#content$/, '')
46
- item.anchor = null
136
+ focus() {
137
+ this.input?.focus()
138
+ }
139
+ }
140
+
141
+ class CxdSearchResults extends HTMLElement {
142
+ connectedCallback() {
143
+ instance.registerResults(this, { keyboardNavigation: true })
144
+
145
+ instance.on('loading', () => this._renderLoading(), this)
146
+ instance.on('results', (result) => this._renderResults(result), this)
147
+ instance.on('error', (error) => this._renderError(error), this)
148
+
149
+ this._onKeydown = this._onKeydown.bind(this)
150
+ this._onClick = this._onClick.bind(this)
151
+ this.addEventListener('keydown', this._onKeydown)
152
+ this.addEventListener('click', this._onClick)
153
+
154
+ this._renderEmpty()
155
+ }
156
+
157
+ disconnectedCallback() {
158
+ this.removeEventListener('keydown', this._onKeydown)
159
+ this.removeEventListener('click', this._onClick)
160
+ this._clearLoadingTimer()
161
+ }
162
+
163
+ _onClick(event) {
164
+ if (event.target.closest('[data-cxd-search-clear-recent]')) {
165
+ event.preventDefault()
166
+ clearRecentVisits()
167
+ this._renderEmpty()
168
+ instance.focusInputAndType(this, '')
169
+ }
170
+ }
171
+
172
+ _getLinks() {
173
+ return [...this.querySelectorAll('.cxd-search-item')]
174
+ }
175
+
176
+ _onKeydown(event) {
177
+ const link = event.target.closest('.cxd-search-item')
178
+ if (!link) return
179
+
180
+ const links = this._getLinks()
181
+ const index = links.indexOf(link)
182
+
183
+ switch (event.key) {
184
+ case 'ArrowDown': {
185
+ const next = links[index + 1]
186
+ if (next) {
187
+ event.preventDefault()
188
+ next.focus()
189
+ }
190
+ break
191
+ }
192
+ case 'ArrowUp': {
193
+ event.preventDefault()
194
+ if (index === 0) {
195
+ instance.focusPreviousInput(this)
196
+ } else {
197
+ links[index - 1].focus()
198
+ }
199
+ break
200
+ }
201
+ case 'Home': {
202
+ if (links[0]) {
203
+ event.preventDefault()
204
+ links[0].focus()
47
205
  }
206
+ break
207
+ }
208
+ case 'End': {
209
+ const last = links.at(-1)
210
+ if (last) {
211
+ event.preventDefault()
212
+ last.focus()
213
+ }
214
+ break
215
+ }
216
+ case 'Backspace': {
217
+ event.preventDefault()
218
+ instance.focusInputAndDelete(this)
219
+ break
220
+ }
221
+ default: {
222
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
223
+ event.preventDefault()
224
+ instance.focusInputAndType(this, event.key)
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ _clearLoadingTimer() {
231
+ if (this._loadingTimer) {
232
+ clearTimeout(this._loadingTimer)
233
+ this._loadingTimer = null
234
+ }
235
+ }
236
+
237
+ _renderEmpty() {
238
+ this._clearLoadingTimer()
239
+ if (instance.searchTerm) return
240
+
241
+ const recents = getRecentVisits()
242
+ if (recents.length === 0) {
243
+ this.innerHTML = `<div class="cxd-search-empty">Start typing to search…</div>`
244
+ return
245
+ }
246
+
247
+ this.innerHTML = `
248
+ <div class="cxd-search-recent">
249
+ <div class="cxd-search-recent-header">
250
+ <span class="cxd-search-recent-label">Recently visited</span>
251
+ <button type="button" class="cxd-search-recent-clear" data-cxd-search-clear-recent>Clear</button>
252
+ </div>
253
+ <ol class="list flush cxd-search-results-list">
254
+ ${recents
255
+ .map((visit) => {
256
+ const isSubLink = visit.url.includes('#')
257
+ return `
258
+ <li class="cxd-search-result">
259
+ ${renderItem({
260
+ url: visit.url,
261
+ title: escapeHtml(visit.title),
262
+ excerpt: escapeHtml(visit.excerpt),
263
+ icon: isSubLink ? 'hashtag-outline' : 'file-outline',
264
+ sub: isSubLink
265
+ })}
266
+ </li>
267
+ `
268
+ })
269
+ .join('')}
270
+ </ol>
271
+ </div>
272
+ `
273
+ }
274
+
275
+ _renderLoading() {
276
+ this._clearLoadingTimer()
277
+ this._loadingTimer = setTimeout(() => {
278
+ this._loadingTimer = null
279
+ this._paintLoading()
280
+ }, LOADING_SKELETON_DELAY_MS)
281
+ }
282
+
283
+ _paintLoading() {
284
+ this.innerHTML = `
285
+ <div class="cxd-search-loading skeleton-glow" role="status" aria-live="polite">
286
+ <span class="visually-hidden">${escapeHtml(instance.translate('searching') || 'Searching…')}</span>
287
+ ${Array.from(
288
+ { length: 3 },
289
+ () => `
290
+ <div class="cxd-search-item">
291
+ <div class="icon cxd-search-icon skeleton">
292
+ </div>
293
+ <div class="cxd-search-item-body">
294
+ <div class="skeleton col-4"></div>
295
+ <div class="skeleton col-12"></div>
296
+ </div>
297
+ </div>
298
+ `
299
+ ).join('')}
300
+ </div>
301
+ `
302
+ }
303
+
304
+ _renderError(error) {
305
+ this._clearLoadingTimer()
306
+ const message =
307
+ error?.message || instance.translate('error_text') || 'Something went wrong with search.'
308
+ this.innerHTML = `<div class="cxd-search-error" role="alert">${escapeHtml(message)}</div>`
309
+ }
310
+
311
+ async _renderResults(searchResult) {
312
+ this._clearLoadingTimer()
313
+ const term = instance.searchTerm
314
+
315
+ if (!term) {
316
+ this._renderEmpty()
317
+ return
318
+ }
319
+
320
+ const rawResults = searchResult?.results ?? []
321
+ if (rawResults.length === 0) {
322
+ const zero =
323
+ instance.translate('zero_results', { SEARCH_TERM: term }) || `No results for "${term}"`
324
+ this.innerHTML = `<div class="cxd-search-empty">${escapeHtml(zero)}</div>`
325
+ return
326
+ }
327
+
328
+ const top = rawResults.slice(0, 5)
329
+ const settled = await Promise.all(top.map((raw) => raw.data().catch(() => null)))
330
+ const data = settled.filter(Boolean)
331
+
332
+ if (instance.searchTerm !== term) return // newer search started
333
+
334
+ this.innerHTML = `
335
+ <ol class="list flush cxd-search-results-list">
336
+ ${data.map((result) => this._renderResult(result)).join('')}
337
+ </ol>
338
+ `
339
+ }
340
+
341
+ _renderResult(result) {
342
+ const title = result.meta?.title || result.url
343
+ const subResults = instance.getDisplaySubResults(result, SUB_RESULTS_LIMIT)
344
+
345
+ const subResultsHtml =
346
+ subResults.length === 0
347
+ ? ''
348
+ : `
349
+ <ul class="list plain cxd-search-subresults">
350
+ ${subResults
351
+ .map(
352
+ (sub) => `
353
+ <li class="list-item">
354
+ ${renderItem({ url: sub.url, title: escapeHtml(sub.title), excerpt: sub.excerpt, icon: 'hashtag-outline', sub: true })}
355
+ </li>
356
+ `
357
+ )
358
+ .join('')}
359
+ </ul>
360
+ `
361
+
362
+ return `
363
+ <li class="list-item cxd-search-result">
364
+ ${renderItem({ url: result.url, title: escapeHtml(title), excerpt: result.excerpt, icon: 'file-outline' })}
365
+ ${subResultsHtml}
366
+ </li>
367
+ `
368
+ }
369
+ }
370
+
371
+ // ─── Registration ─────────────────────────────────────────────────────────────
372
+
373
+ const defineSearchCustomElements = () => {
374
+ if (!customElements.get('cxd-search-input')) {
375
+ customElements.define('cxd-search-input', CxdSearchInput)
376
+ }
377
+ if (!customElements.get('cxd-search-results')) {
378
+ customElements.define('cxd-search-results', CxdSearchResults)
379
+ }
380
+ }
381
+
382
+ // ─── Trigger shortcut hints ───────────────────────────────────────────────────
48
383
 
49
- return item
50
- })
384
+ const isMac = () => {
385
+ if (typeof navigator === 'undefined') return false
386
+ const platform = navigator.userAgentData?.platform || navigator.userAgent || ''
387
+ return /mac|iphone|ipad|ipod/i.test(platform)
388
+ }
389
+
390
+ // Fill in `.cxd-search-trigger-shortcut` slots with the platform-correct hint
391
+ // and reveal them. Rendered hidden by SearchTrigger.astro so non-JS visitors
392
+ // never see a misleading shortcut key.
393
+ const setupTriggerShortcuts = () => {
394
+ const mac = isMac()
395
+ const modifier = mac ? '⌘' : 'Ctrl '
396
+ const ariaKeyshortcut = mac ? 'Meta+K' : 'Control+K'
397
+
398
+ for (const slot of document.querySelectorAll('.cxd-search-trigger-shortcut')) {
399
+ slot.innerHTML = `<kbd class="cxd-search-trigger-key">${modifier}K</kbd>`
400
+ slot.hidden = false
401
+ slot.closest('.cxd-search-trigger')?.setAttribute('aria-keyshortcuts', ariaKeyshortcut)
402
+ }
403
+ }
404
+
405
+ // ─── Dialog open / close (via chassis data-API) ───────────────────────────────
406
+
407
+ const isEditableTarget = (target) => {
408
+ if (!target) return false
409
+ if (target.isContentEditable) return true
410
+ const tag = target.tagName
411
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
412
+ }
413
+
414
+ const openSearchDialog = () => {
415
+ const dialogEl = document.querySelector(DIALOG_SELECTOR)
416
+ if (!dialogEl) return
417
+
418
+ if (dialogEl.open) {
419
+ dialogEl.querySelector('cxd-search-input')?.focus()
420
+ return
421
+ }
422
+
423
+ // Native showModal() — CSS @starting-style handles the entry animation without
424
+ // needing the chassis bundle. Works in both dev (no bundle) and prod.
425
+ dialogEl.showModal()
426
+ requestAnimationFrame(() => {
427
+ dialogEl.querySelector('cxd-search-input')?.focus()
428
+ if (!instance.searchTerm) {
429
+ dialogEl.querySelector('cxd-search-results')?._renderEmpty?.()
51
430
  }
52
431
  })
53
- })()
432
+ }
433
+
434
+ const registerGlobalShortcuts = () => {
435
+ document.addEventListener('keydown', (event) => {
436
+ if (
437
+ (event.metaKey || event.ctrlKey) &&
438
+ !event.altKey &&
439
+ !event.shiftKey &&
440
+ event.key.toLowerCase() === 'k'
441
+ ) {
442
+ event.preventDefault()
443
+ openSearchDialog()
444
+ return
445
+ }
446
+
447
+ if (
448
+ event.key === '/' &&
449
+ !event.metaKey &&
450
+ !event.ctrlKey &&
451
+ !event.altKey &&
452
+ !isEditableTarget(event.target)
453
+ ) {
454
+ event.preventDefault()
455
+ openSearchDialog()
456
+ }
457
+ })
458
+ }
459
+
460
+ // ─── Dialog lifecycle hooks ───────────────────────────────────────────────────
461
+
462
+ const setupDialogResetOnClose = () => {
463
+ const dialogEl = document.querySelector(DIALOG_SELECTOR)
464
+ if (!dialogEl) return
465
+
466
+ // The native `close` event fires whenever the dialog closes — whether via
467
+ // dialogEl.close(), the Esc key, or (in prod) the chassis hide animation.
468
+ // This avoids depending on the chassis bundle being loaded.
469
+ dialogEl.addEventListener('close', () => {
470
+ const inputEl = dialogEl.querySelector('cxd-search-input input')
471
+ if (inputEl) inputEl.value = ''
472
+ instance.triggerSearch('')
473
+ })
474
+ }
475
+
476
+ // ─── Result link handler ──────────────────────────────────────────────────────
477
+
478
+ const registerResultLinkHandler = () => {
479
+ document.addEventListener('click', (event) => {
480
+ const link = event.target.closest('.cxd-search-item')
481
+ if (!link) return
482
+ const dialogEl = link.closest('dialog')
483
+ if (!dialogEl?.matches(DIALOG_SELECTOR)) return
484
+
485
+ const titleEl = link.querySelector('.cxd-search-item-title')
486
+ const excerptEl = link.querySelector('.cxd-search-item-excerpt')
487
+ saveRecentVisit({
488
+ url: link.getAttribute('href') || '',
489
+ title: titleEl?.textContent.trim() || link.textContent.trim(),
490
+ excerpt: excerptEl?.textContent.trim() || ''
491
+ })
492
+
493
+ dialogEl.close()
494
+ })
495
+ }
496
+
497
+ // ─── Init (auto-executes on import) ──────────────────────────────────────────
498
+
499
+ defineSearchCustomElements()
500
+ registerGlobalShortcuts()
501
+ registerResultLinkHandler()
502
+
503
+ if (document.readyState === 'loading') {
504
+ document.addEventListener(
505
+ 'DOMContentLoaded',
506
+ () => {
507
+ setupTriggerShortcuts()
508
+ setupDialogResetOnClose()
509
+ },
510
+ { once: true }
511
+ )
512
+ } else {
513
+ setupTriggerShortcuts()
514
+ setupDialogResetOnClose()
515
+ }
@@ -83,7 +83,7 @@
83
83
  */
84
84
  function updateThemeColor() {
85
85
  const context = getScrollContext()
86
- let color = CONFIG.defaultColor
86
+ let color
87
87
 
88
88
  switch (context) {
89
89
  case 'top':
@@ -14,6 +14,7 @@ import { getSlug, processMarkdownToHtml } from '../libs/utils'
14
14
  import BaseLayout from './BaseLayout.astro'
15
15
  import DocsSidebar from '../components/DocsSidebar.astro'
16
16
  import TableOfContents from '../components/TableOfContents.astro'
17
+ import Icon from '../components/shortcodes/Icon.astro'
17
18
 
18
19
  interface Props {
19
20
  /** Frontmatter data from the docs collection */
@@ -44,29 +45,27 @@ if (frontmatter.toc) {
44
45
  <BaseLayout {...Astro.props} layout="docs" overrides={{ body: bodyProps }}>
45
46
  <div slot="main" class="container fluid cxd-gutter cxd-layout">
46
47
  <aside class="cxd-sidebar">
47
- <div
48
- class="large:offcanvas offcanvas-start"
49
- role="dialog"
50
- tabindex="-1"
48
+ <dialog
49
+ class="max-large:drawer drawer-start"
51
50
  id="cxdSidebar"
52
51
  aria-labelledby="cxdSidebarOffcanvasLabel"
53
52
  >
54
- <div class="offcanvas-header border-bottom">
55
- <h5 class="offcanvas-title" id="cxdSidebarOffcanvasLabel">Browse docs</h5>
53
+ <div class="drawer-header">
54
+ <h2 class="drawer-title" id="cxdSidebarOffcanvasLabel">Browse docs</h2>
56
55
  <button
57
56
  type="button"
58
57
  class="close-button"
59
- data-cx-dismiss="offcanvas"
58
+ data-cx-dismiss="drawer"
60
59
  aria-label="Close"
61
60
  data-cx-target="#cxdSidebar"></button>
62
61
  </div>
63
-
64
- <div class="offcanvas-body">
62
+ <hr class="large:d-none m-0" />
63
+ <div class="drawer-body large:p-0">
65
64
  <slot name="sidebar">
66
65
  <DocsSidebar />
67
66
  </slot>
68
67
  </div>
69
- </div>
68
+ </dialog>
70
69
  </aside>
71
70
 
72
71
  <main class="cxd-main">
@@ -121,14 +120,12 @@ if (frontmatter.toc) {
121
120
  aria-controls="tocContents"
122
121
  >
123
122
  On this page
124
- <svg class="icon icon-small" aria-hidden="true">
125
- <use xlink:href="#chevron-sort-solid" />
126
- </svg>
123
+ <Icon name="chevron-sort-solid" class="icon-small" />
127
124
  </button>
128
125
  <strong class="d-none medium:d-block h6 my-xsmall">On this page</strong>
129
126
  <hr class="d-none medium:d-block my-xsmall" />
130
127
  <div class="collapse cxd-toc-collapse" id="tocContents">
131
- <nav id="TableOfContents">
128
+ <nav id="TableOfContents" aria-label="Table of contents">
132
129
  <slot name="toc">
133
130
  <TableOfContents headings={headings} config={getConfig()} />
134
131
  </slot>
@@ -27,4 +27,5 @@ const url = new URL(path, Astro.site)
27
27
  <meta name="robots" content="noindex" />
28
28
  <meta http-equiv="refresh" content={`0; url=${path}`} />
29
29
  </head>
30
+ <body></body>
30
31
  </html>
@@ -5,8 +5,8 @@
5
5
  * JavaScript imports for site functionality.
6
6
  */
7
7
 
8
- import { getChassisJsProps } from '../../libs/chassis'
9
8
  import type { Layout } from '../../libs/layout'
9
+ import type { HTMLAttributes } from 'astro/types'
10
10
 
11
11
  interface Props {
12
12
  /** Current layout context */
@@ -14,16 +14,21 @@ interface Props {
14
14
  }
15
15
 
16
16
  const { layout } = Astro.props
17
+
18
+ function getChassisJsProps() {
19
+ // __CHASSIS_JS_SRC__ is an optional dev-mode override injected via vite.define by the
20
+ // host project's integration. When set, it replaces the default public-dir bundle path.
21
+ const src =
22
+ (import.meta.env.DEV && (globalThis as any).__CHASSIS_JS_SRC__) ||
23
+ (import.meta.env.PROD ? '/static/js/chassis.bundle.min.js' : '/static/js/chassis.bundle.js')
24
+
25
+ const cxJsLinkProps: HTMLAttributes<'script'> = { type: 'module', src }
26
+
27
+ return cxJsLinkProps
28
+ }
17
29
  ---
18
30
 
19
- <script is:inline {...getChassisJsProps()}></script>
20
31
  <script src="../../js/search.js"></script>
21
- <script>
22
- const tooltipTriggerList = document.querySelectorAll('[data-cx-toggle="tooltip"]')
23
- const tooltipList = [...tooltipTriggerList].map(
24
- (tooltipTriggerEl) => new chassis.Tooltip(tooltipTriggerEl)
25
- )
26
- </script>
32
+ <script is:inline {...getChassisJsProps()}></script>
27
33
  {layout === 'docs' && <script src="../../js/sidebar.js" />}
28
- <script src="../../js/icon-loader.js"></script>
29
34
  <slot name="scripts" />