@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.
- package/README.md +16 -16
- package/index.ts +6 -1
- package/package.json +20 -8
- package/src/components/DocsSidebar.astro +63 -23
- package/src/components/FeatureCard.astro +3 -3
- package/src/components/TableOfContents.astro +8 -7
- package/src/components/ThemeToggler.astro +15 -36
- package/src/components/shortcodes/Blockquote.astro +1 -1
- package/src/components/shortcodes/CSSOnly.astro +6 -2
- package/src/components/shortcodes/Callout.astro +6 -2
- package/src/components/shortcodes/Code.astro +289 -0
- package/src/components/shortcodes/DeprecatedIn.astro +5 -2
- package/src/components/shortcodes/Example.astro +132 -0
- package/src/components/shortcodes/Icon.astro +57 -0
- package/src/components/shortcodes/InFigma.astro +7 -2
- package/src/components/shortcodes/JsDocs.astro +64 -0
- package/src/components/shortcodes/Placeholder.astro +28 -0
- package/src/components/shortcodes/ResizableExample.astro +126 -0
- package/src/components/shortcodes/ScssDocs.astro +108 -0
- package/src/components/shortcodes/ScssDocsSimple.astro +66 -0
- package/src/js/color-modes.js +3 -3
- package/src/js/example-mode.js +75 -0
- package/src/js/search.js +501 -39
- package/src/js/theme-color.js +1 -1
- package/src/layouts/DocsLayout.astro +11 -14
- package/src/layouts/RedirectLayout.astro +1 -0
- package/src/layouts/footer/Scripts.astro +14 -9
- package/src/layouts/head/Analytics.astro +12 -6
- package/src/layouts/head/Favicons.astro +0 -2
- package/src/layouts/head/Head.astro +2 -7
- package/src/layouts/head/Stylesheet.astro +18 -11
- package/src/layouts/header/Header.astro +2 -5
- package/src/layouts/header/Navigation.astro +31 -33
- package/src/layouts/header/SearchDialog.astro +74 -0
- package/src/layouts/header/SearchTrigger.astro +41 -0
- package/src/libs/highlight.ts +91 -0
- package/src/libs/icon.ts +75 -0
- package/src/libs/image.ts +12 -1
- package/src/libs/layout.ts +18 -0
- package/src/libs/markdown.ts +128 -0
- package/src/libs/placeholder.ts +250 -0
- package/src/libs/rehype.ts +0 -17
- package/src/libs/shortcodes.ts +129 -0
- package/src/libs/site.ts +95 -0
- package/src/libs/toc.ts +40 -6
- package/src/libs/utils.ts +34 -82
- package/src/libs/vite.ts +45 -0
- package/src/scss/_anchor.scss +20 -16
- package/src/scss/_callouts.scss +29 -28
- package/src/scss/_content.scss +72 -103
- package/src/scss/_footer.scss +18 -19
- package/src/scss/_layout.scss +56 -63
- package/src/scss/_navbar.scss +19 -123
- package/src/scss/_placeholder-img.scss +11 -7
- package/src/scss/_scrolling.scss +14 -12
- package/src/scss/_search.scss +418 -0
- package/src/scss/_shiki.scss +120 -0
- package/src/scss/_sidebar.scss +137 -132
- package/src/scss/_skippy.scss +6 -4
- package/src/scss/_snippets.scss +181 -0
- package/src/scss/_toc.scss +97 -93
- package/src/scss/_vars.scss +89 -0
- package/src/scss/main.scss +75 -80
- package/src/js/icon-loader.js +0 -229
- package/src/libs/chassis.ts +0 -38
- package/src/scss/_brand.scss +0 -59
- package/src/scss/_buttons.scss +0 -33
- package/src/scss/_clipboard-js.scss +0 -63
- package/src/scss/_code.scss +0 -116
- package/src/scss/_colors.scss +0 -140
- package/src/scss/_docsearch.scss +0 -212
- package/src/scss/_settings.scss +0 -29
- package/src/scss/_syntax.scss +0 -160
- 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
|
|
7
|
-
* Copyright 2024-
|
|
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
|
-
|
|
13
|
-
;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/js/theme-color.js
CHANGED
|
@@ -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
|
-
<
|
|
48
|
-
class="large:
|
|
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="
|
|
55
|
-
<
|
|
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="
|
|
58
|
+
data-cx-dismiss="drawer"
|
|
60
59
|
aria-label="Close"
|
|
61
60
|
data-cx-target="#cxdSidebar"></button>
|
|
62
61
|
</div>
|
|
63
|
-
|
|
64
|
-
<div class="
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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>
|
|
@@ -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" />
|