@chassis-ui/docs 0.1.10 → 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 +8 -8
- package/src/components/NavLink.astro +1 -1
- package/src/components/TableOfContents.astro +8 -7
- package/src/components/ThemeToggler.astro +16 -37
- package/src/components/shortcodes/Blockquote.astro +31 -0
- package/src/components/shortcodes/CSSOnly.astro +22 -0
- package/src/components/shortcodes/Callout.astro +6 -2
- package/src/components/shortcodes/Code.astro +289 -0
- package/src/components/shortcodes/DeprecatedIn.astro +18 -0
- package/src/components/shortcodes/Example.astro +132 -0
- package/src/components/shortcodes/Icon.astro +57 -0
- package/src/components/shortcodes/InFigma.astro +26 -0
- 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 +18 -21
- package/src/layouts/RedirectLayout.astro +1 -0
- package/src/layouts/SingleLayout.astro +3 -3
- package/src/layouts/footer/Footer.astro +5 -5
- package/src/layouts/footer/Scripts.astro +14 -7
- 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 +40 -42
- package/src/layouts/header/SearchDialog.astro +74 -0
- package/src/layouts/header/SearchTrigger.astro +41 -0
- package/src/layouts/header/Skippy.astro +1 -1
- 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 -61
- package/src/scss/_navbar.scss +19 -126
- 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 -81
- 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/_masthead.scss +0 -124
- package/src/scss/_settings.scss +0 -29
- package/src/scss/_syntax.scss +0 -160
- package/src/scss/_variables.scss +0 -78
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 */
|
|
@@ -42,40 +43,38 @@ if (frontmatter.toc) {
|
|
|
42
43
|
---
|
|
43
44
|
|
|
44
45
|
<BaseLayout {...Astro.props} layout="docs" overrides={{ body: bodyProps }}>
|
|
45
|
-
<div slot="main" class="container
|
|
46
|
+
<div slot="main" class="container fluid cxd-gutter cxd-layout">
|
|
46
47
|
<aside class="cxd-sidebar">
|
|
47
|
-
<
|
|
48
|
-
class="
|
|
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">
|
|
73
72
|
<div class="cxd-intro">
|
|
74
73
|
<slot name="intro">
|
|
75
74
|
<div
|
|
76
|
-
class="d-
|
|
75
|
+
class="medium:d-flex medium:flex-row-reverse justify-content-between align-items-start"
|
|
77
76
|
>
|
|
78
|
-
<div class="mb-medium mb-
|
|
77
|
+
<div class="mb-medium medium:mb-0 d-flex text-nowrap">
|
|
79
78
|
{
|
|
80
79
|
// This is needed because we want to show the badge if show_badge isn't present or is set to false
|
|
81
80
|
frontmatter.added &&
|
|
@@ -113,7 +112,7 @@ if (frontmatter.toc) {
|
|
|
113
112
|
frontmatter.toc && headings && (
|
|
114
113
|
<div class="cxd-toc">
|
|
115
114
|
<button
|
|
116
|
-
class="button default mb-large text-decoration-none cxd-toc-toggle d-
|
|
115
|
+
class="button default mb-large text-decoration-none cxd-toc-toggle medium:d-none"
|
|
117
116
|
type="button"
|
|
118
117
|
data-cx-toggle="collapse"
|
|
119
118
|
data-cx-target="#tocContents"
|
|
@@ -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
|
-
<strong class="d-none d-
|
|
129
|
-
<hr class="d-none d-
|
|
125
|
+
<strong class="d-none medium:d-block h6 my-xsmall">On this page</strong>
|
|
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>
|
|
@@ -143,7 +140,7 @@ if (frontmatter.toc) {
|
|
|
143
140
|
frontmatter.sections && (
|
|
144
141
|
<div class="row g-xlarge">
|
|
145
142
|
{frontmatter.sections.map((section: DocSection) => (
|
|
146
|
-
<div class="col-
|
|
143
|
+
<div class="medium:col-6">
|
|
147
144
|
<a
|
|
148
145
|
class="d-block text-decoration-none"
|
|
149
146
|
href={getChassisDocsPath(
|
|
@@ -22,10 +22,10 @@ mainProps['class'] = 'min-vh-100'
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
<BaseLayout {...Astro.props} layout="single" overrides={{ main: mainProps }}>
|
|
25
|
-
<header class="py-xlarge py-
|
|
26
|
-
<div class="container
|
|
25
|
+
<header class="py-xlarge medium:py-3xlarge border-bottom border-subtle">
|
|
26
|
+
<div class="container 2xlarge">
|
|
27
27
|
<div class="row">
|
|
28
|
-
<div class="col col-
|
|
28
|
+
<div class="col large:col-10 2xlarge:col-8">
|
|
29
29
|
<h1 class="mt-0">{title}</h1>
|
|
30
30
|
<p class="font-lead">{description}</p>
|
|
31
31
|
<slot name="header-content" />
|