@chassis-ui/docs 0.3.5 → 0.3.6
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 +12 -4
- package/package.json +2 -8
- package/src/js/search.js +158 -61
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Shared Astro layouts, components, and utilities for Chassis documentation sites.
|
|
4
4
|
|
|
5
|
-
[](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/@chassis-ui/docs) [](https://www.npmjs.com/package/@chassis-ui/docs)
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
@@ -26,11 +26,19 @@ npm install @chassis-ui/docs
|
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
29
|
-
"
|
|
29
|
+
"@astrojs/markdown-remark": "^7.0.0",
|
|
30
|
+
"@chassis-ui/css": ">=0.3.2",
|
|
31
|
+
"@pagefind/component-ui": "^1.0.0",
|
|
32
|
+
"@shikijs/transformers": "^4.3.1",
|
|
33
|
+
"astro": "^7.0.0",
|
|
34
|
+
"htmlparser2": "^12.0.0",
|
|
35
|
+
"rehype-autolink-headings": "^7.0.0",
|
|
36
|
+
"sass": "^1.101.0",
|
|
37
|
+
"shiki": "^4.3.1"
|
|
30
38
|
}
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
In practice, sites also install the rest of the Chassis stack — `@chassis-ui/
|
|
41
|
+
In practice, sites also install the rest of the Chassis stack — `@chassis-ui/tokens`, `@chassis-ui/icons` — to render the layouts and styles correctly.
|
|
34
42
|
|
|
35
43
|
## Usage
|
|
36
44
|
|
|
@@ -55,7 +63,7 @@ import { generateTOC, processImage } from '@chassis-ui/docs'
|
|
|
55
63
|
|
|
56
64
|
| Subpath | Contents |
|
|
57
65
|
| ------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
58
|
-
| `@chassis-ui/docs` | Library functions: `
|
|
66
|
+
| `@chassis-ui/docs` | Library functions: `highlight`, `image`, `layout`, `markdown`, `placeholder`, `rehype`, `shortcodes`, `site`, `toc`, `utils` |
|
|
59
67
|
| `@chassis-ui/docs/layouts/*` | `BaseLayout`, `DocsLayout`, `RedirectLayout`, `SingleLayout` |
|
|
60
68
|
| `@chassis-ui/docs/components/*` | `DocsSidebar`, `FeatureCard`, `NavLink`, `ResponsiveImage`, `TableOfContents`, `ThemeToggler` |
|
|
61
69
|
| `@chassis-ui/docs/shortcodes/*` | MDX shortcode components |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chassis-ui/docs",
|
|
3
3
|
"description": "Shared Astro components and layouts for Chassis documentation sites.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -54,11 +54,6 @@
|
|
|
54
54
|
"sass": "^1.101.0",
|
|
55
55
|
"shiki": "^4.3.1"
|
|
56
56
|
},
|
|
57
|
-
"peerDependenciesMeta": {
|
|
58
|
-
"@pagefind/component-ui": {
|
|
59
|
-
"optional": true
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
57
|
"dependencies": {
|
|
63
58
|
"github-slugger": "^2.0.0",
|
|
64
59
|
"image-size": "^2.0.2",
|
|
@@ -71,7 +66,6 @@
|
|
|
71
66
|
"devDependencies": {
|
|
72
67
|
"@types/hast": "^3.0.4",
|
|
73
68
|
"astro": "^7.0.6",
|
|
74
|
-
"unified": "^11.0.5"
|
|
75
|
-
"vite": "^8.1.3"
|
|
69
|
+
"unified": "^11.0.5"
|
|
76
70
|
}
|
|
77
71
|
}
|
package/src/js/search.js
CHANGED
|
@@ -41,12 +41,10 @@ const CHASSIS_SITES = [
|
|
|
41
41
|
{ slug: 'figma', label: 'Figma' }
|
|
42
42
|
]
|
|
43
43
|
|
|
44
|
-
// Hosts where every project's index is reachable at its
|
|
45
|
-
// path via Vercel's proxy rewrites. Everywhere else
|
|
46
|
-
// `*.vercel.app` preview deployments) only this site's own
|
|
47
|
-
// merging would 404
|
|
48
|
-
// `mergeIndex` call — it throws and takes the whole search down with it — so
|
|
49
|
-
// merging is only attempted on these hosts.
|
|
44
|
+
// Hosts where every project's index is *expected* to be reachable at its
|
|
45
|
+
// root-relative bundle path via Vercel's proxy rewrites. Everywhere else
|
|
46
|
+
// (local dev, direct `*.vercel.app` preview deployments) only this site's own
|
|
47
|
+
// pages exist, so merging would always 404 — not attempted there at all.
|
|
50
48
|
const CANONICAL_HOSTS = new Set(['chassis-ui.com', 'staging.chassis-ui.com'])
|
|
51
49
|
|
|
52
50
|
const getBundlePath = (slug) => (slug === 'website' ? '/pagefind/' : `/${slug}/pagefind/`)
|
|
@@ -69,19 +67,17 @@ const currentSite = getCurrentSite()
|
|
|
69
67
|
// `/tokens/tokens/docs/...`. Force it back to `/` for every index.
|
|
70
68
|
const BASE_URL = '/'
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
// NOTE: we deliberately do NOT pass `mergeIndex` here. `@pagefind/component-ui`
|
|
71
|
+
// awaits every entry in that array back-to-back with no try/catch — if a
|
|
72
|
+
// single sibling site's index 404s (e.g. mid-rollout, one repo deployed
|
|
73
|
+
// before another), the whole `__doLoad__` throws and search breaks entirely,
|
|
74
|
+
// including this site's own results. We instead merge additional sites
|
|
75
|
+
// ourselves afterwards, in `mergeAdditionalSites()` below, so a single
|
|
76
|
+
// unreachable sibling is skipped rather than taking everything down.
|
|
80
77
|
const instance = getInstanceManager().getInstance('default', {
|
|
81
78
|
bundlePath: getBundlePath(currentSite.slug),
|
|
82
79
|
baseUrl: BASE_URL,
|
|
83
|
-
mergeFilter: { site: currentSite.slug }
|
|
84
|
-
mergeIndex
|
|
80
|
+
mergeFilter: { site: currentSite.slug }
|
|
85
81
|
})
|
|
86
82
|
|
|
87
83
|
// Default filter scope: the website (hub) defaults to searching everywhere;
|
|
@@ -91,6 +87,99 @@ if (currentSite.slug !== 'website') {
|
|
|
91
87
|
instance.searchFilters = { site: currentSite.slug }
|
|
92
88
|
}
|
|
93
89
|
|
|
90
|
+
let mergeAttempted = false
|
|
91
|
+
|
|
92
|
+
// How long to wait for a sibling's Pagefind entry file before assuming it's
|
|
93
|
+
// unreachable (bad deploy, rollout in progress, host down, etc.).
|
|
94
|
+
const PROBE_TIMEOUT_MS = 4000
|
|
95
|
+
|
|
96
|
+
// Mirrors Pagefind's own language selection in `PagefindInstance.findIndex`:
|
|
97
|
+
// exact `<html lang>` match, then its base subtag, then the language with the
|
|
98
|
+
// most pages. Needed so the probe below checks the same chunk `init()` will
|
|
99
|
+
// actually request.
|
|
100
|
+
const getPreferredLanguageHash = (entry) => {
|
|
101
|
+
const languages = entry?.languages
|
|
102
|
+
if (!languages) return null
|
|
103
|
+
const htmlLang = document.documentElement.getAttribute('lang')?.toLowerCase()
|
|
104
|
+
if (htmlLang && languages[htmlLang]) return languages[htmlLang].hash
|
|
105
|
+
const base = htmlLang?.split('-')[0]
|
|
106
|
+
if (base && languages[base]) return languages[base].hash
|
|
107
|
+
const [topLanguage] = Object.values(languages).sort((a, b) => b.page_count - a.page_count)
|
|
108
|
+
return topLanguage?.hash ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Checks whether a sibling site's Pagefind bundle actually exists, without
|
|
113
|
+
* touching the shared `pf` instance. This matters because a *failed*
|
|
114
|
+
* `pf.mergeIndex()` call corrupts that instance beyond recovery — there is no
|
|
115
|
+
* public API to detach a bad merge, and every future search (including this
|
|
116
|
+
* site's own) throws the same error afterwards.
|
|
117
|
+
*
|
|
118
|
+
* `init()` performs two fetches, not one: `pagefind-entry.json`, then the
|
|
119
|
+
* language-specific `pagefind.<hash>.pf_meta` chunk it points to. Checking
|
|
120
|
+
* only the entry file misses exactly the failure this is meant to guard
|
|
121
|
+
* against — a deploy mid-rollout where the entry file has already updated to
|
|
122
|
+
* reference a hash whose chunk hasn't finished uploading (or was purged) yet.
|
|
123
|
+
* So we probe both before ever calling `mergeIndex`.
|
|
124
|
+
*/
|
|
125
|
+
const isBundleReachable = async (bundlePath) => {
|
|
126
|
+
const controller = new AbortController()
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS)
|
|
128
|
+
try {
|
|
129
|
+
const entryResponse = await fetch(`${bundlePath}pagefind-entry.json`, {
|
|
130
|
+
signal: controller.signal
|
|
131
|
+
})
|
|
132
|
+
if (!entryResponse.ok) return false
|
|
133
|
+
|
|
134
|
+
const hash = getPreferredLanguageHash(await entryResponse.json())
|
|
135
|
+
if (!hash) return false
|
|
136
|
+
|
|
137
|
+
const metaResponse = await fetch(`${bundlePath}pagefind.${hash}.pf_meta`, {
|
|
138
|
+
signal: controller.signal
|
|
139
|
+
})
|
|
140
|
+
return metaResponse.ok
|
|
141
|
+
} catch {
|
|
142
|
+
return false
|
|
143
|
+
} finally {
|
|
144
|
+
clearTimeout(timer)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Loads this site's own Pagefind bundle, then merges every other Chassis
|
|
150
|
+
* site's index in, one at a time. Each sibling is probed first (see
|
|
151
|
+
* `isBundleReachable`) so an unreachable one — a bad deploy, a rollout still
|
|
152
|
+
* in progress across the (separate) Chassis repos, etc. — is silently skipped
|
|
153
|
+
* instead of breaking search for everyone. Safe to call multiple times; only
|
|
154
|
+
* does real work once.
|
|
155
|
+
*/
|
|
156
|
+
const mergeAdditionalSites = async () => {
|
|
157
|
+
if (mergeAttempted) return
|
|
158
|
+
mergeAttempted = true
|
|
159
|
+
|
|
160
|
+
await instance.triggerLoad()
|
|
161
|
+
const pf = instance.__pagefind__
|
|
162
|
+
if (!pf || !isCanonicalHost()) return
|
|
163
|
+
|
|
164
|
+
const siblings = CHASSIS_SITES.filter((site) => site.slug !== currentSite.slug)
|
|
165
|
+
await Promise.allSettled(
|
|
166
|
+
siblings.map(async (site) => {
|
|
167
|
+
const bundlePath = getBundlePath(site.slug)
|
|
168
|
+
if (!(await isBundleReachable(bundlePath))) {
|
|
169
|
+
console.warn(
|
|
170
|
+
`Chassis search: "${site.slug}" index not reachable at ${bundlePath}, skipping`
|
|
171
|
+
)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await pf.mergeIndex(bundlePath, { baseUrl: BASE_URL, mergeFilter: { site: site.slug } })
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn(`Chassis search: failed to merge the "${site.slug}" index`, error)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
94
183
|
// ─── Recent visits ────────────────────────────────────────────────────────────
|
|
95
184
|
// Opt-out via Do Not Track; silently no-op when storage is unavailable.
|
|
96
185
|
|
|
@@ -137,20 +226,40 @@ const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'
|
|
|
137
226
|
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => HTML_ESCAPES[char])
|
|
138
227
|
|
|
139
228
|
// Single template for every clickable row: top-level results, sub-results, and
|
|
140
|
-
// recently visited items. `
|
|
141
|
-
//
|
|
142
|
-
|
|
229
|
+
// recently visited items. `titleHtml`/`excerptHtml` name the contract, not just
|
|
230
|
+
// describe it: callers must pass already-safe HTML, either by running plain
|
|
231
|
+
// text through `escapeHtml` (recents) or by using Pagefind's own pre-marked
|
|
232
|
+
// excerpt (search results, which embed `<mark>` highlights).
|
|
233
|
+
const renderItem = ({ url, titleHtml, excerptHtml = '', icon, sub = false }) => `
|
|
143
234
|
<a class="cxd-search-item${sub ? ' cxd-search-subitem' : ''}" href="${escapeHtml(url)}">
|
|
144
235
|
<svg class="icon cxd-search-item-icon" width="16" height="16" aria-hidden="true">
|
|
145
|
-
<use href="#${icon}"></use>
|
|
236
|
+
<use href="/static/icons/chassis-icons.svg#${icon}"></use>
|
|
146
237
|
</svg>
|
|
147
238
|
<div class="cxd-search-item-body">
|
|
148
|
-
<div class="cxd-search-item-title">${
|
|
149
|
-
${
|
|
239
|
+
<div class="cxd-search-item-title">${titleHtml}</div>
|
|
240
|
+
${excerptHtml ? `<div class="cxd-search-item-excerpt">${excerptHtml}</div>` : ''}
|
|
150
241
|
</div>
|
|
151
242
|
</a>
|
|
152
243
|
`
|
|
153
244
|
|
|
245
|
+
// Shared wrapper for every rendered list of rows (recents, top-level results).
|
|
246
|
+
const renderResultsList = (itemsHtml) =>
|
|
247
|
+
`<ol class="list flush cxd-search-results-list">${itemsHtml}</ol>`
|
|
248
|
+
|
|
249
|
+
// Wires `handlers` (eventType -> bound listener) onto `target` and returns a
|
|
250
|
+
// cleanup function that removes them all — the bind/track/remove dance every
|
|
251
|
+
// element below otherwise repeats by hand in connectedCallback/disconnectedCallback.
|
|
252
|
+
const addListeners = (target, handlers) => {
|
|
253
|
+
for (const [type, handler] of Object.entries(handlers)) {
|
|
254
|
+
target.addEventListener(type, handler)
|
|
255
|
+
}
|
|
256
|
+
return () => {
|
|
257
|
+
for (const [type, handler] of Object.entries(handlers)) {
|
|
258
|
+
target.removeEventListener(type, handler)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
154
263
|
// ─── Custom elements ──────────────────────────────────────────────────────────
|
|
155
264
|
|
|
156
265
|
class CxdSearchInput extends HTMLElement {
|
|
@@ -161,23 +270,16 @@ class CxdSearchInput extends HTMLElement {
|
|
|
161
270
|
this.inputEl = this.input
|
|
162
271
|
instance.registerInput(this, { keyboardNavigation: true })
|
|
163
272
|
|
|
164
|
-
this.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.input.addEventListener(
|
|
170
|
-
'focus',
|
|
171
|
-
() => {
|
|
172
|
-
instance.triggerLoad()
|
|
173
|
-
},
|
|
174
|
-
{ once: true }
|
|
175
|
-
)
|
|
273
|
+
this._removeListeners = addListeners(this.input, {
|
|
274
|
+
input: this._onInput.bind(this),
|
|
275
|
+
keydown: this._onKeydown.bind(this)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
this.input.addEventListener('focus', () => mergeAdditionalSites(), { once: true })
|
|
176
279
|
}
|
|
177
280
|
|
|
178
281
|
disconnectedCallback() {
|
|
179
|
-
this.
|
|
180
|
-
this.input?.removeEventListener('keydown', this._onKeydown)
|
|
282
|
+
this._removeListeners?.()
|
|
181
283
|
}
|
|
182
284
|
|
|
183
285
|
_onInput(event) {
|
|
@@ -213,17 +315,16 @@ class CxdSearchResults extends HTMLElement {
|
|
|
213
315
|
instance.on('results', (result) => this._renderResults(result), this)
|
|
214
316
|
instance.on('error', (error) => this._renderError(error), this)
|
|
215
317
|
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
318
|
+
this._removeListeners = addListeners(this, {
|
|
319
|
+
keydown: this._onKeydown.bind(this),
|
|
320
|
+
click: this._onClick.bind(this)
|
|
321
|
+
})
|
|
220
322
|
|
|
221
323
|
this._renderEmpty()
|
|
222
324
|
}
|
|
223
325
|
|
|
224
326
|
disconnectedCallback() {
|
|
225
|
-
this.
|
|
226
|
-
this.removeEventListener('click', this._onClick)
|
|
327
|
+
this._removeListeners?.()
|
|
227
328
|
this._clearLoadingTimer()
|
|
228
329
|
}
|
|
229
330
|
|
|
@@ -317,24 +418,24 @@ class CxdSearchResults extends HTMLElement {
|
|
|
317
418
|
<span class="cxd-search-recent-label">Recently visited</span>
|
|
318
419
|
<button type="button" class="cxd-search-recent-clear" data-cxd-search-clear-recent>Clear</button>
|
|
319
420
|
</div>
|
|
320
|
-
|
|
321
|
-
|
|
421
|
+
${renderResultsList(
|
|
422
|
+
recents
|
|
322
423
|
.map((visit) => {
|
|
323
424
|
const isSubLink = visit.url.includes('#')
|
|
324
425
|
return `
|
|
325
426
|
<li class="cxd-search-result">
|
|
326
427
|
${renderItem({
|
|
327
428
|
url: visit.url,
|
|
328
|
-
|
|
329
|
-
|
|
429
|
+
titleHtml: escapeHtml(visit.title),
|
|
430
|
+
excerptHtml: escapeHtml(visit.excerpt),
|
|
330
431
|
icon: isSubLink ? 'hashtag-outline' : 'file-outline',
|
|
331
432
|
sub: isSubLink
|
|
332
433
|
})}
|
|
333
434
|
</li>
|
|
334
435
|
`
|
|
335
436
|
})
|
|
336
|
-
.join('')
|
|
337
|
-
|
|
437
|
+
.join('')
|
|
438
|
+
)}
|
|
338
439
|
</div>
|
|
339
440
|
`
|
|
340
441
|
}
|
|
@@ -398,11 +499,7 @@ class CxdSearchResults extends HTMLElement {
|
|
|
398
499
|
|
|
399
500
|
if (instance.searchTerm !== term) return // newer search started
|
|
400
501
|
|
|
401
|
-
this.innerHTML =
|
|
402
|
-
<ol class="list flush cxd-search-results-list">
|
|
403
|
-
${data.map((result) => this._renderResult(result)).join('')}
|
|
404
|
-
</ol>
|
|
405
|
-
`
|
|
502
|
+
this.innerHTML = renderResultsList(data.map((result) => this._renderResult(result)).join(''))
|
|
406
503
|
}
|
|
407
504
|
|
|
408
505
|
_renderResult(result) {
|
|
@@ -418,7 +515,7 @@ class CxdSearchResults extends HTMLElement {
|
|
|
418
515
|
.map(
|
|
419
516
|
(sub) => `
|
|
420
517
|
<li class="list-item">
|
|
421
|
-
${renderItem({ url: sub.url,
|
|
518
|
+
${renderItem({ url: sub.url, titleHtml: escapeHtml(sub.title), excerptHtml: sub.excerpt, icon: 'hashtag-outline', sub: true })}
|
|
422
519
|
</li>
|
|
423
520
|
`
|
|
424
521
|
)
|
|
@@ -428,7 +525,7 @@ class CxdSearchResults extends HTMLElement {
|
|
|
428
525
|
|
|
429
526
|
return `
|
|
430
527
|
<li class="list-item cxd-search-result">
|
|
431
|
-
${renderItem({ url: result.url,
|
|
528
|
+
${renderItem({ url: result.url, titleHtml: escapeHtml(title), excerptHtml: result.excerpt, icon: 'file-outline' })}
|
|
432
529
|
${subResultsHtml}
|
|
433
530
|
</li>
|
|
434
531
|
`
|
|
@@ -437,9 +534,8 @@ class CxdSearchResults extends HTMLElement {
|
|
|
437
534
|
|
|
438
535
|
// ─── Site filter ──────────────────────────────────────────────────────────────
|
|
439
536
|
// Dropdown letting the visitor scope results to a single project, or back out
|
|
440
|
-
// to "Everywhere".
|
|
441
|
-
//
|
|
442
|
-
// meaningful to filter.
|
|
537
|
+
// to "Everywhere". Always rendered (see `CxdSearchFilter` below) — filtering to
|
|
538
|
+
// a site that hasn't actually been merged in just yields no results.
|
|
443
539
|
|
|
444
540
|
const FILTER_OPTIONS = [{ slug: '', label: 'Everywhere' }, ...CHASSIS_SITES]
|
|
445
541
|
|
|
@@ -459,12 +555,13 @@ class CxdSearchFilter extends HTMLElement {
|
|
|
459
555
|
).join('')
|
|
460
556
|
this.select.value = currentSite.slug === 'website' ? '' : currentSite.slug
|
|
461
557
|
|
|
462
|
-
this.
|
|
463
|
-
|
|
558
|
+
this._removeListeners = addListeners(this.select, {
|
|
559
|
+
change: this._onChange.bind(this)
|
|
560
|
+
})
|
|
464
561
|
}
|
|
465
562
|
|
|
466
563
|
disconnectedCallback() {
|
|
467
|
-
this.
|
|
564
|
+
this._removeListeners?.()
|
|
468
565
|
}
|
|
469
566
|
|
|
470
567
|
_onChange() {
|