@chassis-ui/docs 0.3.4 → 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 +245 -40
- package/src/layouts/header/SearchDialog.astro +4 -0
- package/src/scss/_search.scss +360 -344
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
|
@@ -22,7 +22,163 @@ const LOADING_SKELETON_DELAY_MS = 200
|
|
|
22
22
|
const RECENT_STORAGE_KEY = 'cxd:search:recent'
|
|
23
23
|
const RECENT_LIMIT = 5
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// ─── Multi-site search ────────────────────────────────────────────────────────
|
|
26
|
+
// Every Chassis project deploys its own Pagefind index under its own path
|
|
27
|
+
// prefix (e.g. `/tokens/pagefind/`), matching the prefix chassis-ui.com's
|
|
28
|
+
// Vercel rewrites proxy that project under (see `ref/VERCEL_CONFIG.md`).
|
|
29
|
+
// This lets one shared instance merge every project's index into a single,
|
|
30
|
+
// site-filterable search — no cross-origin requests required, since
|
|
31
|
+
// `chassis-ui.com/<slug>/...` and `<slug>.vercel.app/<slug>/...` both resolve
|
|
32
|
+
// the same root-relative bundle path.
|
|
33
|
+
|
|
34
|
+
/** All Chassis projects with their own Pagefind index, in nav order. `website` is the hub (root-level index, no path prefix). */
|
|
35
|
+
const CHASSIS_SITES = [
|
|
36
|
+
{ slug: 'website', label: 'Website' },
|
|
37
|
+
{ slug: 'tokens', label: 'Tokens' },
|
|
38
|
+
{ slug: 'css', label: 'CSS' },
|
|
39
|
+
{ slug: 'assets', label: 'Assets' },
|
|
40
|
+
{ slug: 'icons', label: 'Icons' },
|
|
41
|
+
{ slug: 'figma', label: 'Figma' }
|
|
42
|
+
]
|
|
43
|
+
|
|
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.
|
|
48
|
+
const CANONICAL_HOSTS = new Set(['chassis-ui.com', 'staging.chassis-ui.com'])
|
|
49
|
+
|
|
50
|
+
const getBundlePath = (slug) => (slug === 'website' ? '/pagefind/' : `/${slug}/pagefind/`)
|
|
51
|
+
|
|
52
|
+
const getCurrentSite = () => {
|
|
53
|
+
const [, firstSegment] = typeof location === 'undefined' ? [] : location.pathname.split('/')
|
|
54
|
+
return CHASSIS_SITES.find((site) => site.slug === firstSegment) ?? CHASSIS_SITES[0]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isCanonicalHost = () =>
|
|
58
|
+
typeof location !== 'undefined' && CANONICAL_HOSTS.has(location.hostname)
|
|
59
|
+
|
|
60
|
+
const currentSite = getCurrentSite()
|
|
61
|
+
|
|
62
|
+
// Pagefind derives `baseUrl` from `bundlePath` by stripping the trailing
|
|
63
|
+
// `pagefind` segment (e.g. `/tokens/pagefind/` -> `/tokens/`), then prepends
|
|
64
|
+
// that to every result's raw URL. Those raw URLs are already site-root-relative
|
|
65
|
+
// (e.g. `tokens/docs/...`, because that's where the page actually lives inside
|
|
66
|
+
// `_site`), so the auto-derived baseUrl would double up the prefix into
|
|
67
|
+
// `/tokens/tokens/docs/...`. Force it back to `/` for every index.
|
|
68
|
+
const BASE_URL = '/'
|
|
69
|
+
|
|
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.
|
|
77
|
+
const instance = getInstanceManager().getInstance('default', {
|
|
78
|
+
bundlePath: getBundlePath(currentSite.slug),
|
|
79
|
+
baseUrl: BASE_URL,
|
|
80
|
+
mergeFilter: { site: currentSite.slug }
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Default filter scope: the website (hub) defaults to searching everywhere;
|
|
84
|
+
// every project site defaults to its own results, but can still be widened
|
|
85
|
+
// or switched via the `cxd-search-filter` control.
|
|
86
|
+
if (currentSite.slug !== 'website') {
|
|
87
|
+
instance.searchFilters = { site: currentSite.slug }
|
|
88
|
+
}
|
|
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
|
+
}
|
|
26
182
|
|
|
27
183
|
// ─── Recent visits ────────────────────────────────────────────────────────────
|
|
28
184
|
// Opt-out via Do Not Track; silently no-op when storage is unavailable.
|
|
@@ -70,20 +226,40 @@ const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'
|
|
|
70
226
|
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => HTML_ESCAPES[char])
|
|
71
227
|
|
|
72
228
|
// Single template for every clickable row: top-level results, sub-results, and
|
|
73
|
-
// recently visited items. `
|
|
74
|
-
//
|
|
75
|
-
|
|
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 }) => `
|
|
76
234
|
<a class="cxd-search-item${sub ? ' cxd-search-subitem' : ''}" href="${escapeHtml(url)}">
|
|
77
235
|
<svg class="icon cxd-search-item-icon" width="16" height="16" aria-hidden="true">
|
|
78
|
-
<use href="#${icon}"></use>
|
|
236
|
+
<use href="/static/icons/chassis-icons.svg#${icon}"></use>
|
|
79
237
|
</svg>
|
|
80
238
|
<div class="cxd-search-item-body">
|
|
81
|
-
<div class="cxd-search-item-title">${
|
|
82
|
-
${
|
|
239
|
+
<div class="cxd-search-item-title">${titleHtml}</div>
|
|
240
|
+
${excerptHtml ? `<div class="cxd-search-item-excerpt">${excerptHtml}</div>` : ''}
|
|
83
241
|
</div>
|
|
84
242
|
</a>
|
|
85
243
|
`
|
|
86
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
|
+
|
|
87
263
|
// ─── Custom elements ──────────────────────────────────────────────────────────
|
|
88
264
|
|
|
89
265
|
class CxdSearchInput extends HTMLElement {
|
|
@@ -94,23 +270,16 @@ class CxdSearchInput extends HTMLElement {
|
|
|
94
270
|
this.inputEl = this.input
|
|
95
271
|
instance.registerInput(this, { keyboardNavigation: true })
|
|
96
272
|
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.input.addEventListener(
|
|
103
|
-
'focus',
|
|
104
|
-
() => {
|
|
105
|
-
instance.triggerLoad()
|
|
106
|
-
},
|
|
107
|
-
{ once: true }
|
|
108
|
-
)
|
|
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 })
|
|
109
279
|
}
|
|
110
280
|
|
|
111
281
|
disconnectedCallback() {
|
|
112
|
-
this.
|
|
113
|
-
this.input?.removeEventListener('keydown', this._onKeydown)
|
|
282
|
+
this._removeListeners?.()
|
|
114
283
|
}
|
|
115
284
|
|
|
116
285
|
_onInput(event) {
|
|
@@ -146,17 +315,16 @@ class CxdSearchResults extends HTMLElement {
|
|
|
146
315
|
instance.on('results', (result) => this._renderResults(result), this)
|
|
147
316
|
instance.on('error', (error) => this._renderError(error), this)
|
|
148
317
|
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
318
|
+
this._removeListeners = addListeners(this, {
|
|
319
|
+
keydown: this._onKeydown.bind(this),
|
|
320
|
+
click: this._onClick.bind(this)
|
|
321
|
+
})
|
|
153
322
|
|
|
154
323
|
this._renderEmpty()
|
|
155
324
|
}
|
|
156
325
|
|
|
157
326
|
disconnectedCallback() {
|
|
158
|
-
this.
|
|
159
|
-
this.removeEventListener('click', this._onClick)
|
|
327
|
+
this._removeListeners?.()
|
|
160
328
|
this._clearLoadingTimer()
|
|
161
329
|
}
|
|
162
330
|
|
|
@@ -250,24 +418,24 @@ class CxdSearchResults extends HTMLElement {
|
|
|
250
418
|
<span class="cxd-search-recent-label">Recently visited</span>
|
|
251
419
|
<button type="button" class="cxd-search-recent-clear" data-cxd-search-clear-recent>Clear</button>
|
|
252
420
|
</div>
|
|
253
|
-
|
|
254
|
-
|
|
421
|
+
${renderResultsList(
|
|
422
|
+
recents
|
|
255
423
|
.map((visit) => {
|
|
256
424
|
const isSubLink = visit.url.includes('#')
|
|
257
425
|
return `
|
|
258
426
|
<li class="cxd-search-result">
|
|
259
427
|
${renderItem({
|
|
260
428
|
url: visit.url,
|
|
261
|
-
|
|
262
|
-
|
|
429
|
+
titleHtml: escapeHtml(visit.title),
|
|
430
|
+
excerptHtml: escapeHtml(visit.excerpt),
|
|
263
431
|
icon: isSubLink ? 'hashtag-outline' : 'file-outline',
|
|
264
432
|
sub: isSubLink
|
|
265
433
|
})}
|
|
266
434
|
</li>
|
|
267
435
|
`
|
|
268
436
|
})
|
|
269
|
-
.join('')
|
|
270
|
-
|
|
437
|
+
.join('')
|
|
438
|
+
)}
|
|
271
439
|
</div>
|
|
272
440
|
`
|
|
273
441
|
}
|
|
@@ -331,11 +499,7 @@ class CxdSearchResults extends HTMLElement {
|
|
|
331
499
|
|
|
332
500
|
if (instance.searchTerm !== term) return // newer search started
|
|
333
501
|
|
|
334
|
-
this.innerHTML =
|
|
335
|
-
<ol class="list flush cxd-search-results-list">
|
|
336
|
-
${data.map((result) => this._renderResult(result)).join('')}
|
|
337
|
-
</ol>
|
|
338
|
-
`
|
|
502
|
+
this.innerHTML = renderResultsList(data.map((result) => this._renderResult(result)).join(''))
|
|
339
503
|
}
|
|
340
504
|
|
|
341
505
|
_renderResult(result) {
|
|
@@ -351,7 +515,7 @@ class CxdSearchResults extends HTMLElement {
|
|
|
351
515
|
.map(
|
|
352
516
|
(sub) => `
|
|
353
517
|
<li class="list-item">
|
|
354
|
-
${renderItem({ url: sub.url,
|
|
518
|
+
${renderItem({ url: sub.url, titleHtml: escapeHtml(sub.title), excerptHtml: sub.excerpt, icon: 'hashtag-outline', sub: true })}
|
|
355
519
|
</li>
|
|
356
520
|
`
|
|
357
521
|
)
|
|
@@ -361,13 +525,51 @@ class CxdSearchResults extends HTMLElement {
|
|
|
361
525
|
|
|
362
526
|
return `
|
|
363
527
|
<li class="list-item cxd-search-result">
|
|
364
|
-
${renderItem({ url: result.url,
|
|
528
|
+
${renderItem({ url: result.url, titleHtml: escapeHtml(title), excerptHtml: result.excerpt, icon: 'file-outline' })}
|
|
365
529
|
${subResultsHtml}
|
|
366
530
|
</li>
|
|
367
531
|
`
|
|
368
532
|
}
|
|
369
533
|
}
|
|
370
534
|
|
|
535
|
+
// ─── Site filter ──────────────────────────────────────────────────────────────
|
|
536
|
+
// Dropdown letting the visitor scope results to a single project, or back out
|
|
537
|
+
// to "Everywhere". Always rendered (see `CxdSearchFilter` below) — filtering to
|
|
538
|
+
// a site that hasn't actually been merged in just yields no results.
|
|
539
|
+
|
|
540
|
+
const FILTER_OPTIONS = [{ slug: '', label: 'Everywhere' }, ...CHASSIS_SITES]
|
|
541
|
+
|
|
542
|
+
class CxdSearchFilter extends HTMLElement {
|
|
543
|
+
connectedCallback() {
|
|
544
|
+
this.select = this.querySelector('select')
|
|
545
|
+
if (!this.select) return
|
|
546
|
+
|
|
547
|
+
// Always rendered — even outside merging (local dev, direct preview
|
|
548
|
+
// deployments) the current site's own results are tagged with `site`, so
|
|
549
|
+
// "Everywhere" and the current site both work; switching to a *different*
|
|
550
|
+
// site just yields no results until it's actually merged in.
|
|
551
|
+
this.hidden = false
|
|
552
|
+
|
|
553
|
+
this.select.innerHTML = FILTER_OPTIONS.map(
|
|
554
|
+
(option) => `<option value="${option.slug}">${escapeHtml(option.label)}</option>`
|
|
555
|
+
).join('')
|
|
556
|
+
this.select.value = currentSite.slug === 'website' ? '' : currentSite.slug
|
|
557
|
+
|
|
558
|
+
this._removeListeners = addListeners(this.select, {
|
|
559
|
+
change: this._onChange.bind(this)
|
|
560
|
+
})
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
disconnectedCallback() {
|
|
564
|
+
this._removeListeners?.()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
_onChange() {
|
|
568
|
+
const value = this.select.value
|
|
569
|
+
instance.triggerSearchWithFilters(instance.searchTerm, value ? { site: value } : {})
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
371
573
|
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
372
574
|
|
|
373
575
|
const defineSearchCustomElements = () => {
|
|
@@ -377,6 +579,9 @@ const defineSearchCustomElements = () => {
|
|
|
377
579
|
if (!customElements.get('cxd-search-results')) {
|
|
378
580
|
customElements.define('cxd-search-results', CxdSearchResults)
|
|
379
581
|
}
|
|
582
|
+
if (!customElements.get('cxd-search-filter')) {
|
|
583
|
+
customElements.define('cxd-search-filter', CxdSearchFilter)
|
|
584
|
+
}
|
|
380
585
|
}
|
|
381
586
|
|
|
382
587
|
// ─── Trigger shortcut hints ───────────────────────────────────────────────────
|
|
@@ -44,6 +44,10 @@ const { id = 'cxdSearchDialog', placeholder = 'Search docs…' } = Astro.props
|
|
|
44
44
|
/>
|
|
45
45
|
</div>
|
|
46
46
|
</cxd-search-input>
|
|
47
|
+
<cxd-search-filter class="cxd-search-filter" hidden>
|
|
48
|
+
<label class="visually-hidden" for={`${id}-filter`}>Search in</label>
|
|
49
|
+
<select id={`${id}-filter`} class="form-input cxd-search-filter-select"></select>
|
|
50
|
+
</cxd-search-filter>
|
|
47
51
|
<button
|
|
48
52
|
type="button"
|
|
49
53
|
class="close-button"
|
package/src/scss/_search.scss
CHANGED
|
@@ -1,418 +1,434 @@
|
|
|
1
1
|
@use "@chassis-ui/css/scss/mixins" as *;
|
|
2
2
|
@use "@chassis-ui/css/scss/placeholders" as *;
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
@layer custom {
|
|
5
|
+
//
|
|
6
|
+
// Pagefind search UI
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
.cxd-search {
|
|
10
|
+
position: relative;
|
|
11
|
+
|
|
12
|
+
@include media-breakpoint-up(large) {
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: 20%;
|
|
15
|
+
right: 13rem;
|
|
16
|
+
width: 200px;
|
|
17
|
+
|
|
18
|
+
[dir="rtl"] & {
|
|
19
|
+
right: auto;
|
|
20
|
+
left: 13rem;
|
|
21
|
+
}
|
|
20
22
|
}
|
|
23
|
+
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
}
|
|
24
26
|
|
|
27
|
+
// Trigger button
|
|
28
|
+
//
|
|
29
|
+
// Mobile (< md): icon-only, no chrome.
|
|
30
|
+
// md+: expands to a faux input with label and ⌘K shortcut hint.
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
cursor: pointer;
|
|
47
|
-
background-color: transparent;
|
|
48
|
-
border: 0;
|
|
49
|
-
@include border-radius(var(--cx-border-radius-medium));
|
|
50
|
-
@include transition(color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out);
|
|
51
|
-
|
|
52
|
-
&:focus-visible {
|
|
53
|
-
@include focus-ring(true);
|
|
54
|
-
}
|
|
32
|
+
.cxd-search-trigger {
|
|
33
|
+
// @extend %form-input;
|
|
34
|
+
--cx-padding-x: var(--cx-space-xsmall);
|
|
35
|
+
--cx-padding-y: .375rem;
|
|
36
|
+
--cx-icon-color: var(--cx-fg-color);
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
gap: .5rem;
|
|
39
|
+
align-items: center;
|
|
40
|
+
margin-block: .125rem;
|
|
41
|
+
// padding-inline-end: .5rem;
|
|
42
|
+
// padding: .25rem .5rem;
|
|
43
|
+
@include exclude-strokes(form-input);
|
|
44
|
+
@include font-size(var(--cx-font-size-small));
|
|
45
|
+
color: var(--cx-fg-subtle);
|
|
46
|
+
text-align: start;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
background-color: transparent;
|
|
49
|
+
border: 0;
|
|
50
|
+
@include border-radius(var(--cx-border-radius-medium));
|
|
51
|
+
@include transition(color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out);
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
--cx-icon-color: var(--cx-fg-subtle);
|
|
60
|
-
width: 100%;
|
|
61
|
-
background-color: var(--cx-bg-main);
|
|
62
|
-
@include border();
|
|
53
|
+
&:focus-visible {
|
|
54
|
+
@include focus-ring(true);
|
|
55
|
+
}
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
--cx-icon-
|
|
67
|
-
--cx-
|
|
57
|
+
@include media-breakpoint-up(large) {
|
|
58
|
+
--cx-border-color: var(--cx-border-subtle);
|
|
59
|
+
--cx-icon-size: var(--cx-icon-small);
|
|
60
|
+
--cx-icon-color: var(--cx-fg-subtle);
|
|
61
|
+
width: 100%;
|
|
62
|
+
background-color: var(--cx-bg-main);
|
|
63
|
+
@include border();
|
|
64
|
+
|
|
65
|
+
&:hover {
|
|
66
|
+
// --fg-color: var(--cx-fg-main);
|
|
67
|
+
--cx-icon-color: var(--cx-fg-color);
|
|
68
|
+
--cx-border-color: var(--cx-border-main);
|
|
69
|
+
}
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
|
-
}
|
|
71
72
|
|
|
72
|
-
.cxd-search-trigger-icon {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
73
|
+
.cxd-search-trigger-icon {
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
color: var(--cx-fg-subtle);
|
|
76
|
+
}
|
|
76
77
|
|
|
77
|
-
.cxd-search-trigger-label,
|
|
78
|
-
.cxd-search-trigger-shortcut {
|
|
79
|
-
|
|
78
|
+
.cxd-search-trigger-label,
|
|
79
|
+
.cxd-search-trigger-shortcut {
|
|
80
|
+
display: none;
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
@include media-breakpoint-up(large) {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
}
|
|
83
85
|
}
|
|
84
|
-
}
|
|
85
86
|
|
|
86
|
-
.cxd-search-trigger-label {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
87
|
+
.cxd-search-trigger-label {
|
|
88
|
+
flex: 1;
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
text-overflow: ellipsis;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
}
|
|
92
93
|
|
|
93
|
-
.cxd-search-trigger-shortcut {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
94
|
+
.cxd-search-trigger-shortcut {
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
gap: .125rem;
|
|
97
|
+
}
|
|
97
98
|
|
|
98
|
-
.cxd-search-trigger-key {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
99
|
+
.cxd-search-trigger-key {
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
min-width: 1.25rem;
|
|
104
|
+
padding: .125rem .375rem;
|
|
105
|
+
font-family: var(--cx-font-family-code);
|
|
106
|
+
@include font-size(var(--cx-font-size-small));
|
|
107
|
+
@include line-height(var(--cx-line-height-small));
|
|
108
|
+
color: var(--cx-fg-subtle);
|
|
109
|
+
background-color: var(--cx-bg-evident);
|
|
110
|
+
// border: var(--cx-border-width) solid var(--cx-border-color);
|
|
111
|
+
@include border-radius(var(--cx-border-radius-small));
|
|
112
|
+
}
|
|
112
113
|
|
|
113
|
-
// Search dialog shell
|
|
114
|
-
//
|
|
115
|
-
// Top-aligned overlay using the existing `.dialog` surface.
|
|
116
|
-
// Sizing and position make it feel like a command palette.
|
|
114
|
+
// Search dialog shell
|
|
115
|
+
//
|
|
116
|
+
// Top-aligned overlay using the existing `.dialog` surface.
|
|
117
|
+
// Sizing and position make it feel like a command palette.
|
|
118
|
+
|
|
119
|
+
.cxd-search-dialog {
|
|
120
|
+
--cx-dialog-margin: 1.5rem;
|
|
121
|
+
--cx-dialog-padding: .75rem;
|
|
122
|
+
--cx-dialog-header-padding: .75rem;
|
|
123
|
+
--cx-dialog-footer-padding: .5rem .75rem;
|
|
124
|
+
--cx-dialog-border-radius: var(--cx-border-radius-large);
|
|
125
|
+
|
|
126
|
+
align-self: flex-start;
|
|
127
|
+
width: 640px;
|
|
128
|
+
max-height: min(80dvh, 720px);
|
|
129
|
+
margin-block-start: 3rem;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
background-color: var(--cx-bg-even);
|
|
132
|
+
|
|
133
|
+
@include media-breakpoint-down(small) {
|
|
134
|
+
max-height: calc(100dvh - 4rem);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
--cx-dialog-margin: 1.5rem;
|
|
120
|
-
--cx-dialog-padding: .75rem;
|
|
121
|
-
--cx-dialog-header-padding: .75rem;
|
|
122
|
-
--cx-dialog-footer-padding: .5rem .75rem;
|
|
123
|
-
--cx-dialog-border-radius: var(--cx-border-radius-large);
|
|
138
|
+
// Header / search form
|
|
124
139
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
.cxd-search-form {
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-wrap: nowrap;
|
|
143
|
+
gap: .75rem;
|
|
144
|
+
align-items: center;
|
|
145
|
+
padding: var(--cx-space-medium);
|
|
146
|
+
border-bottom: 0;
|
|
147
|
+
}
|
|
131
148
|
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
cxd-search-input {
|
|
150
|
+
flex-grow: 1;
|
|
134
151
|
}
|
|
135
|
-
}
|
|
136
152
|
|
|
137
|
-
//
|
|
153
|
+
// Site filter dropdown — only shown when other project indexes were merged
|
|
154
|
+
// in (see `search.js`), letting the visitor scope results to one project or
|
|
155
|
+
// back out to "Everywhere".
|
|
156
|
+
.cxd-search-filter {
|
|
157
|
+
flex-shrink: 0;
|
|
158
|
+
}
|
|
138
159
|
|
|
139
|
-
.cxd-search-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
// .cxd-search-filter-select {
|
|
161
|
+
// --cx-padding-y: .375rem;
|
|
162
|
+
// --cx-padding-x: .5rem;
|
|
163
|
+
// width: auto;
|
|
164
|
+
// @include font-size(var(--cx-font-size-small));
|
|
165
|
+
// }
|
|
166
|
+
// .cxd-search-input {
|
|
167
|
+
// flex: 1;
|
|
168
|
+
// width: 100%;
|
|
169
|
+
// min-width: 0;
|
|
170
|
+
// background-color: transparent;
|
|
171
|
+
// border-color: transparent;
|
|
172
|
+
// box-shadow: none;
|
|
173
|
+
|
|
174
|
+
// &:focus {
|
|
175
|
+
// background-color: transparent;
|
|
176
|
+
// border-color: transparent;
|
|
177
|
+
// box-shadow: none;
|
|
178
|
+
// }
|
|
179
|
+
// }
|
|
147
180
|
|
|
148
|
-
cxd-search-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
// flex: 1;
|
|
153
|
-
// width: 100%;
|
|
154
|
-
// min-width: 0;
|
|
155
|
-
// background-color: transparent;
|
|
156
|
-
// border-color: transparent;
|
|
157
|
-
// box-shadow: none;
|
|
158
|
-
|
|
159
|
-
// &:focus {
|
|
160
|
-
// background-color: transparent;
|
|
161
|
-
// border-color: transparent;
|
|
162
|
-
// box-shadow: none;
|
|
163
|
-
// }
|
|
164
|
-
// }
|
|
165
|
-
|
|
166
|
-
// .cxd-search-close {
|
|
167
|
-
// flex-shrink: 0;
|
|
168
|
-
// margin-inline-start: 0;
|
|
169
|
-
// }
|
|
170
|
-
|
|
171
|
-
// Body / results
|
|
172
|
-
|
|
173
|
-
.cxd-search-body {
|
|
174
|
-
padding: 0 var(--cx-space-medium) var(--cx-space-medium);
|
|
175
|
-
}
|
|
181
|
+
// .cxd-search-close {
|
|
182
|
+
// flex-shrink: 0;
|
|
183
|
+
// margin-inline-start: 0;
|
|
184
|
+
// }
|
|
176
185
|
|
|
177
|
-
|
|
178
|
-
display: block;
|
|
179
|
-
min-height: 6rem;
|
|
180
|
-
border: 0;
|
|
181
|
-
}
|
|
186
|
+
// Body / results
|
|
182
187
|
|
|
183
|
-
.cxd-search-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
.cxd-search-body {
|
|
189
|
+
padding: 0 var(--cx-space-medium) var(--cx-space-medium);
|
|
190
|
+
}
|
|
186
191
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.list-item {
|
|
191
|
-
padding: 0;
|
|
192
|
+
.cxd-search-results {
|
|
193
|
+
display: block;
|
|
194
|
+
min-height: 6rem;
|
|
192
195
|
border: 0;
|
|
193
196
|
}
|
|
194
|
-
}
|
|
195
197
|
|
|
196
|
-
.cxd-search-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// gap: var(--cx-space-xsmall);
|
|
200
|
-
gap: 0;
|
|
201
|
-
padding: 0;
|
|
202
|
-
}
|
|
198
|
+
.cxd-search-results-list {
|
|
199
|
+
// --cx-list-border-width: 0;
|
|
200
|
+
// --cx-list-border-radius: 0;
|
|
203
201
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Single row template: top-level results, sub-results, and recent visits.
|
|
214
|
-
.cxd-search-item {
|
|
215
|
-
--cx-icon-size: var(--cx-icon-medium);
|
|
216
|
-
--cx-icon-color: var(--cx-fg-subtle);
|
|
217
|
-
position: relative;
|
|
218
|
-
display: grid;
|
|
219
|
-
grid-template-columns: auto 1fr;
|
|
220
|
-
column-gap: .5rem;
|
|
221
|
-
align-items: center;
|
|
222
|
-
padding: var(--cx-space-xsmall);
|
|
223
|
-
text-decoration: none;
|
|
224
|
-
background-color: var(--cx-bg-main);
|
|
225
|
-
@include border();
|
|
226
|
-
@include border-radius(var(--cx-border-radius-medium));
|
|
227
|
-
|
|
228
|
-
mark {
|
|
229
|
-
background: none;
|
|
202
|
+
gap: var(--cx-space-small);
|
|
203
|
+
padding-block-start: var(--cx-space-xsmall);
|
|
204
|
+
list-style: none;
|
|
205
|
+
.list-item {
|
|
206
|
+
padding: 0;
|
|
207
|
+
border: 0;
|
|
208
|
+
}
|
|
230
209
|
}
|
|
231
210
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
211
|
+
.cxd-search-result {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
// gap: var(--cx-space-xsmall);
|
|
215
|
+
gap: 0;
|
|
216
|
+
padding: 0;
|
|
235
217
|
}
|
|
236
218
|
|
|
237
|
-
|
|
219
|
+
// Default-active state: while focus is in the search input, the first result
|
|
220
|
+
// is highlighted as the Enter target. Once the user arrows into the list,
|
|
221
|
+
// :focus-within takes over.
|
|
222
|
+
.cxd-search-results:not(:focus-within) .cxd-search-result:first-child > .cxd-search-item {
|
|
223
|
+
@include focus-ring(true);
|
|
224
|
+
--cx-focus-ring-offset: -2px;
|
|
238
225
|
background-color: var(--cx-bg-main);
|
|
239
226
|
}
|
|
240
227
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
228
|
+
// Single row template: top-level results, sub-results, and recent visits.
|
|
229
|
+
.cxd-search-item {
|
|
230
|
+
--cx-icon-size: var(--cx-icon-medium);
|
|
231
|
+
--cx-icon-color: var(--cx-fg-subtle);
|
|
232
|
+
position: relative;
|
|
233
|
+
display: grid;
|
|
234
|
+
grid-template-columns: auto 1fr;
|
|
235
|
+
column-gap: .5rem;
|
|
236
|
+
align-items: center;
|
|
237
|
+
padding: var(--cx-space-xsmall);
|
|
238
|
+
text-decoration: none;
|
|
239
|
+
background-color: var(--cx-bg-main);
|
|
240
|
+
@include border();
|
|
241
|
+
@include border-radius(var(--cx-border-radius-medium));
|
|
246
242
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
243
|
+
mark {
|
|
244
|
+
background: none;
|
|
245
|
+
}
|
|
251
246
|
|
|
252
|
-
.cxd-search-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
247
|
+
&:has(+ .cxd-search-subresults) {
|
|
248
|
+
border-block-end-width: 0;
|
|
249
|
+
@include border-bottom-radius(0);
|
|
250
|
+
}
|
|
256
251
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
252
|
+
&:hover {
|
|
253
|
+
background-color: var(--cx-bg-main);
|
|
254
|
+
}
|
|
261
255
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
256
|
+
&:focus-visible {
|
|
257
|
+
--cx-focus-ring-offset: -2px;
|
|
258
|
+
z-index: 1;
|
|
259
|
+
@include focus-ring(true);
|
|
260
|
+
}
|
|
266
261
|
|
|
267
|
-
.cxd-search-item-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
// &:not(.cxd-search-item-sub) {
|
|
263
|
+
// border: var(--cx-border-width) solid var(--cx-border-color);
|
|
264
|
+
// }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.cxd-search-item-icon {
|
|
268
|
+
// margin-block-start: .15rem;
|
|
269
|
+
color: var(--cx-fg-subtle);
|
|
270
|
+
}
|
|
275
271
|
|
|
276
|
-
.cxd-search-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
align-items: center;
|
|
280
|
-
padding-inline-start: var(--cx-space-4xlarge);
|
|
281
|
-
// padding-inline-start: var(--cx-space-medium);
|
|
282
|
-
|
|
283
|
-
&::before {
|
|
284
|
-
position: absolute;
|
|
285
|
-
inset-block: 15%;
|
|
286
|
-
inset-inline: 0;
|
|
287
|
-
width: var(--cx-border-width);
|
|
288
|
-
height: 70%;
|
|
289
|
-
margin-inline-start: 1.25rem;
|
|
290
|
-
content: " ";
|
|
291
|
-
background-color: var(--cx-fg-subtle);
|
|
292
|
-
}
|
|
293
|
-
&::after {
|
|
294
|
-
position: absolute;
|
|
295
|
-
inset: 0;
|
|
296
|
-
width: .75rem;
|
|
297
|
-
height: var(--cx-border-width);
|
|
298
|
-
margin-block: auto;
|
|
299
|
-
margin-inline-start: 1.25rem;
|
|
300
|
-
content: " ";
|
|
301
|
-
background-color: var(--cx-fg-subtle);
|
|
272
|
+
.cxd-search-item-body {
|
|
273
|
+
grid-column: 2;
|
|
274
|
+
min-width: 0;
|
|
302
275
|
}
|
|
276
|
+
|
|
303
277
|
.cxd-search-item-title {
|
|
278
|
+
font-weight: var(--cx-font-weight-strong);
|
|
279
|
+
color: var(--cx-fg-main);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.cxd-search-item-excerpt {
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
color: var(--cx-fg-subtle);
|
|
285
|
+
text-overflow: ellipsis;
|
|
286
|
+
white-space: nowrap;
|
|
304
287
|
@include font-size(var(--cx-font-size-small));
|
|
305
288
|
@include line-height(var(--cx-line-height-small));
|
|
306
289
|
}
|
|
307
290
|
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.cxd-search-subresults {
|
|
311
|
-
display: flex;
|
|
312
|
-
flex-direction: column;
|
|
313
|
-
// gap: var(--cx-space-xsmall);
|
|
314
|
-
gap: 0;
|
|
315
|
-
// padding-inline-start: var(--cx-space-large);
|
|
316
291
|
.cxd-search-subitem {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
292
|
+
--cx-icon-size: var(--cx-icon-small);
|
|
293
|
+
--cx-icon-color: var(--cx-fg-subtle);
|
|
294
|
+
align-items: center;
|
|
295
|
+
padding-inline-start: var(--cx-space-4xlarge);
|
|
296
|
+
// padding-inline-start: var(--cx-space-medium);
|
|
297
|
+
|
|
298
|
+
&::before {
|
|
299
|
+
position: absolute;
|
|
300
|
+
inset-block: 15%;
|
|
301
|
+
inset-inline: 0;
|
|
302
|
+
width: var(--cx-border-width);
|
|
303
|
+
height: 70%;
|
|
304
|
+
margin-inline-start: 1.25rem;
|
|
305
|
+
content: " ";
|
|
306
|
+
background-color: var(--cx-fg-subtle);
|
|
307
|
+
}
|
|
308
|
+
&::after {
|
|
309
|
+
position: absolute;
|
|
310
|
+
inset: 0;
|
|
311
|
+
width: .75rem;
|
|
312
|
+
height: var(--cx-border-width);
|
|
313
|
+
margin-block: auto;
|
|
314
|
+
margin-inline-start: 1.25rem;
|
|
315
|
+
content: " ";
|
|
316
|
+
background-color: var(--cx-fg-subtle);
|
|
317
|
+
}
|
|
318
|
+
.cxd-search-item-title {
|
|
319
|
+
@include font-size(var(--cx-font-size-small));
|
|
320
|
+
@include line-height(var(--cx-line-height-small));
|
|
325
321
|
}
|
|
326
322
|
|
|
327
323
|
}
|
|
328
|
-
}
|
|
329
324
|
|
|
330
|
-
|
|
325
|
+
.cxd-search-subresults {
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
// gap: var(--cx-space-xsmall);
|
|
329
|
+
gap: 0;
|
|
330
|
+
// padding-inline-start: var(--cx-space-large);
|
|
331
|
+
.cxd-search-subitem {
|
|
332
|
+
border-block-end-width: 0;
|
|
333
|
+
@include border-radius(0);
|
|
334
|
+
&:is(:last-child > .cxd-search-subitem) {
|
|
335
|
+
border-block-end-width: var(--cx-border-width);
|
|
336
|
+
@include border-bottom-radius(var(--cx-border-radius));
|
|
337
|
+
&::before {
|
|
338
|
+
height: 35%;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
331
341
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
gap: .5rem;
|
|
335
|
-
align-items: center;
|
|
336
|
-
justify-content: space-between;
|
|
337
|
-
padding-block: .5rem;
|
|
338
|
-
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
339
344
|
|
|
340
|
-
|
|
341
|
-
@include font-size(var(--cx-font-size-xsmall));
|
|
342
|
-
font-weight: 600;
|
|
343
|
-
color: var(--cx-fg-subtle);
|
|
344
|
-
text-transform: uppercase;
|
|
345
|
-
letter-spacing: .04em;
|
|
346
|
-
}
|
|
345
|
+
// Recently visited
|
|
347
346
|
|
|
348
|
-
.cxd-search-recent-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
border: 0;
|
|
355
|
-
@include border-radius(var(--cx-border-radius-medium));
|
|
356
|
-
@include transition(color .15s ease-in-out, background-color .15s ease-in-out);
|
|
357
|
-
|
|
358
|
-
&:hover {
|
|
359
|
-
color: var(--cx-fg-main);
|
|
360
|
-
background-color: var(--cx-bg-main);
|
|
347
|
+
.cxd-search-recent-header {
|
|
348
|
+
display: flex;
|
|
349
|
+
gap: .5rem;
|
|
350
|
+
align-items: center;
|
|
351
|
+
justify-content: space-between;
|
|
352
|
+
padding-block: .5rem;
|
|
361
353
|
}
|
|
362
354
|
|
|
363
|
-
|
|
364
|
-
@include
|
|
355
|
+
.cxd-search-recent-label {
|
|
356
|
+
@include font-size(var(--cx-font-size-xsmall));
|
|
357
|
+
font-weight: 600;
|
|
358
|
+
color: var(--cx-fg-subtle);
|
|
359
|
+
text-transform: uppercase;
|
|
360
|
+
letter-spacing: .04em;
|
|
365
361
|
}
|
|
366
|
-
}
|
|
367
362
|
|
|
368
|
-
|
|
363
|
+
.cxd-search-recent-clear {
|
|
364
|
+
padding: .125rem .375rem;
|
|
365
|
+
@include font-size(var(--cx-font-size-xsmall));
|
|
366
|
+
color: var(--cx-fg-subtle);
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
background-color: transparent;
|
|
369
|
+
border: 0;
|
|
370
|
+
@include border-radius(var(--cx-border-radius-medium));
|
|
371
|
+
@include transition(color .15s ease-in-out, background-color .15s ease-in-out);
|
|
369
372
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
color: var(--cx-fg-subtle);
|
|
375
|
-
text-align: center;
|
|
376
|
-
}
|
|
373
|
+
&:hover {
|
|
374
|
+
color: var(--cx-fg-main);
|
|
375
|
+
background-color: var(--cx-bg-main);
|
|
376
|
+
}
|
|
377
377
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
378
|
+
&:focus-visible {
|
|
379
|
+
@include focus-ring(true);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
display: flex;
|
|
384
|
-
flex-direction: column;
|
|
385
|
-
gap: var(--cx-space-small);
|
|
386
|
-
}
|
|
383
|
+
// Empty / loading / error states
|
|
387
384
|
|
|
385
|
+
.cxd-search-empty,
|
|
386
|
+
.cxd-search-error {
|
|
387
|
+
padding: 2rem 1rem;
|
|
388
|
+
@include font-size(var(--cx-font-size-small));
|
|
389
|
+
color: var(--cx-fg-subtle);
|
|
390
|
+
text-align: center;
|
|
391
|
+
}
|
|
388
392
|
|
|
389
|
-
.cxd-search-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
+
.cxd-search-error {
|
|
394
|
+
color: var(--cx-danger-fg-main);
|
|
395
|
+
}
|
|
393
396
|
|
|
394
|
-
.cxd-search-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
color: var(--cx-fg-subtle);
|
|
400
|
-
background: none;
|
|
401
|
-
border: 0;
|
|
402
|
-
}
|
|
397
|
+
.cxd-search-loading {
|
|
398
|
+
display: flex;
|
|
399
|
+
flex-direction: column;
|
|
400
|
+
gap: var(--cx-space-small);
|
|
401
|
+
}
|
|
403
402
|
|
|
404
|
-
.cxd-search-shortcut-key {
|
|
405
|
-
display: inline-flex;
|
|
406
|
-
align-items: center;
|
|
407
|
-
justify-content: center;
|
|
408
|
-
min-width: 1.25rem;
|
|
409
|
-
min-height: 1.25rem;
|
|
410
|
-
padding: 0 .25rem;
|
|
411
|
-
background-color: var(--cx-bg-main);
|
|
412
|
-
}
|
|
413
403
|
|
|
414
|
-
.cxd-search-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
404
|
+
.cxd-search-footer {
|
|
405
|
+
padding-block: var(--cx-space-small);
|
|
406
|
+
background-color: var(--cx-bg-evident);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.cxd-search-shortcuts-list > .list-item {
|
|
410
|
+
display: inline-flex;
|
|
411
|
+
gap: .25rem;
|
|
412
|
+
align-items: center;
|
|
413
|
+
padding: 0 .5rem;
|
|
414
|
+
color: var(--cx-fg-subtle);
|
|
415
|
+
background: none;
|
|
416
|
+
border: 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.cxd-search-shortcut-key {
|
|
420
|
+
display: inline-flex;
|
|
421
|
+
align-items: center;
|
|
422
|
+
justify-content: center;
|
|
423
|
+
min-width: 1.25rem;
|
|
424
|
+
min-height: 1.25rem;
|
|
425
|
+
padding: 0 .25rem;
|
|
426
|
+
background-color: var(--cx-bg-main);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.cxd-search-shortcut-label {
|
|
430
|
+
padding-inline-start: .25rem;
|
|
431
|
+
@include font-size(var(--cx-font-size-small));
|
|
432
|
+
@include line-height(var(--cx-line-height-small));
|
|
433
|
+
}
|
|
418
434
|
}
|