@chassis-ui/docs 0.3.3 → 0.3.5

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Shared Astro layouts, components, and utilities for Chassis documentation sites.
4
4
 
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Version: 0.3.3](https://img.shields.io/badge/Version-0.3.3-blue.svg)](https://www.npmjs.com/package/@chassis-ui/docs) [![npm](https://img.shields.io/npm/v/@chassis-ui/docs.svg)](https://www.npmjs.com/package/@chassis-ui/docs)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Version: 0.3.5](https://img.shields.io/badge/Version-0.3.5-blue.svg)](https://www.npmjs.com/package/@chassis-ui/docs) [![npm](https://img.shields.io/npm/v/@chassis-ui/docs.svg)](https://www.npmjs.com/package/@chassis-ui/docs)
6
6
 
7
7
  ## Overview
8
8
 
package/index.ts CHANGED
@@ -9,4 +9,3 @@ export * from './src/libs/shortcodes'
9
9
  export * from './src/libs/site'
10
10
  export * from './src/libs/toc'
11
11
  export * from './src/libs/utils'
12
- export * from './src/libs/vite'
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.3",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@astrojs/markdown-remark": "^7.0.0",
48
- "@chassis-ui/css": ">=0.3.1",
48
+ "@chassis-ui/css": ">=0.3.2",
49
49
  "@pagefind/component-ui": "^1.0.0",
50
50
  "@shikijs/transformers": "^4.3.1",
51
51
  "astro": "^7.0.0",
package/src/js/search.js CHANGED
@@ -22,7 +22,74 @@ const LOADING_SKELETON_DELAY_MS = 200
22
22
  const RECENT_STORAGE_KEY = 'cxd:search:recent'
23
23
  const RECENT_LIMIT = 5
24
24
 
25
- const instance = getInstanceManager().getInstance('default')
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 reachable at its root-relative bundle
45
+ // path via Vercel's proxy rewrites. Everywhere else (local dev, direct
46
+ // `*.vercel.app` preview deployments) only this site's own pages exist, so
47
+ // merging would 404. Pagefind has no built-in resilience for a failed
48
+ // `mergeIndex` call — it throws and takes the whole search down with it — so
49
+ // merging is only attempted on these hosts.
50
+ const CANONICAL_HOSTS = new Set(['chassis-ui.com', 'staging.chassis-ui.com'])
51
+
52
+ const getBundlePath = (slug) => (slug === 'website' ? '/pagefind/' : `/${slug}/pagefind/`)
53
+
54
+ const getCurrentSite = () => {
55
+ const [, firstSegment] = typeof location === 'undefined' ? [] : location.pathname.split('/')
56
+ return CHASSIS_SITES.find((site) => site.slug === firstSegment) ?? CHASSIS_SITES[0]
57
+ }
58
+
59
+ const isCanonicalHost = () =>
60
+ typeof location !== 'undefined' && CANONICAL_HOSTS.has(location.hostname)
61
+
62
+ const currentSite = getCurrentSite()
63
+
64
+ // Pagefind derives `baseUrl` from `bundlePath` by stripping the trailing
65
+ // `pagefind` segment (e.g. `/tokens/pagefind/` -> `/tokens/`), then prepends
66
+ // that to every result's raw URL. Those raw URLs are already site-root-relative
67
+ // (e.g. `tokens/docs/...`, because that's where the page actually lives inside
68
+ // `_site`), so the auto-derived baseUrl would double up the prefix into
69
+ // `/tokens/tokens/docs/...`. Force it back to `/` for every index.
70
+ const BASE_URL = '/'
71
+
72
+ const mergeIndex = isCanonicalHost()
73
+ ? CHASSIS_SITES.filter((site) => site.slug !== currentSite.slug).map((site) => ({
74
+ bundlePath: getBundlePath(site.slug),
75
+ baseUrl: BASE_URL,
76
+ mergeFilter: { site: site.slug }
77
+ }))
78
+ : []
79
+
80
+ const instance = getInstanceManager().getInstance('default', {
81
+ bundlePath: getBundlePath(currentSite.slug),
82
+ baseUrl: BASE_URL,
83
+ mergeFilter: { site: currentSite.slug },
84
+ mergeIndex
85
+ })
86
+
87
+ // Default filter scope: the website (hub) defaults to searching everywhere;
88
+ // every project site defaults to its own results, but can still be widened
89
+ // or switched via the `cxd-search-filter` control.
90
+ if (currentSite.slug !== 'website') {
91
+ instance.searchFilters = { site: currentSite.slug }
92
+ }
26
93
 
27
94
  // ─── Recent visits ────────────────────────────────────────────────────────────
28
95
  // Opt-out via Do Not Track; silently no-op when storage is unavailable.
@@ -368,6 +435,44 @@ class CxdSearchResults extends HTMLElement {
368
435
  }
369
436
  }
370
437
 
438
+ // ─── Site filter ──────────────────────────────────────────────────────────────
439
+ // Dropdown letting the visitor scope results to a single project, or back out
440
+ // to "Everywhere". Only rendered when other sites were actually merged in
441
+ // (see `mergeIndex` above) — with a single, un-merged index there's nothing
442
+ // meaningful to filter.
443
+
444
+ const FILTER_OPTIONS = [{ slug: '', label: 'Everywhere' }, ...CHASSIS_SITES]
445
+
446
+ class CxdSearchFilter extends HTMLElement {
447
+ connectedCallback() {
448
+ this.select = this.querySelector('select')
449
+ if (!this.select) return
450
+
451
+ // Always rendered — even outside merging (local dev, direct preview
452
+ // deployments) the current site's own results are tagged with `site`, so
453
+ // "Everywhere" and the current site both work; switching to a *different*
454
+ // site just yields no results until it's actually merged in.
455
+ this.hidden = false
456
+
457
+ this.select.innerHTML = FILTER_OPTIONS.map(
458
+ (option) => `<option value="${option.slug}">${escapeHtml(option.label)}</option>`
459
+ ).join('')
460
+ this.select.value = currentSite.slug === 'website' ? '' : currentSite.slug
461
+
462
+ this._onChange = this._onChange.bind(this)
463
+ this.select.addEventListener('change', this._onChange)
464
+ }
465
+
466
+ disconnectedCallback() {
467
+ this.select?.removeEventListener('change', this._onChange)
468
+ }
469
+
470
+ _onChange() {
471
+ const value = this.select.value
472
+ instance.triggerSearchWithFilters(instance.searchTerm, value ? { site: value } : {})
473
+ }
474
+ }
475
+
371
476
  // ─── Registration ─────────────────────────────────────────────────────────────
372
477
 
373
478
  const defineSearchCustomElements = () => {
@@ -377,6 +482,9 @@ const defineSearchCustomElements = () => {
377
482
  if (!customElements.get('cxd-search-results')) {
378
483
  customElements.define('cxd-search-results', CxdSearchResults)
379
484
  }
485
+ if (!customElements.get('cxd-search-filter')) {
486
+ customElements.define('cxd-search-filter', CxdSearchFilter)
487
+ }
380
488
  }
381
489
 
382
490
  // ─── Trigger shortcut hints ───────────────────────────────────────────────────
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import type { Layout } from '../../libs/layout'
9
- import type { HTMLAttributes } from 'astro/types'
10
9
 
11
10
  interface Props {
12
11
  /** Current layout context */
@@ -14,21 +13,11 @@ interface Props {
14
13
  }
15
14
 
16
15
  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
- }
29
16
  ---
30
17
 
31
18
  <script src="../../js/search.js"></script>
32
- <script is:inline {...getChassisJsProps()}></script>
19
+ <script>
20
+ import '@chassis-ui/css'
21
+ </script>
33
22
  {layout === 'docs' && <script src="../../js/sidebar.js" />}
34
23
  <slot name="scripts" />
@@ -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"
@@ -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
- // Pagefind search UI
6
- //
7
-
8
- .cxd-search {
9
- position: relative;
10
-
11
- @include media-breakpoint-up(large) {
12
- position: absolute;
13
- top: 20%;
14
- right: 13rem;
15
- width: 200px;
16
-
17
- [dir="rtl"] & {
18
- right: auto;
19
- left: 13rem;
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
- // Trigger button
27
- //
28
- // Mobile (< md): icon-only, no chrome.
29
- // md+: expands to a faux input with label and ⌘K shortcut hint.
30
-
31
- .cxd-search-trigger {
32
- // @extend %form-input;
33
- --cx-padding-x: var(--cx-space-xsmall);
34
- --cx-padding-y: .375rem;
35
- --cx-icon-color: var(--cx-fg-color);
36
- display: inline-flex;
37
- gap: .5rem;
38
- align-items: center;
39
- margin-block: .125rem;
40
- // padding-inline-end: .5rem;
41
- // padding: .25rem .5rem;
42
- @include exclude-strokes(form-input);
43
- @include font-size(var(--cx-font-size-small));
44
- color: var(--cx-fg-subtle);
45
- text-align: start;
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
- @include media-breakpoint-up(large) {
57
- --cx-border-color: var(--cx-border-subtle);
58
- --cx-icon-size: var(--cx-icon-small);
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
- &:hover {
65
- // --fg-color: var(--cx-fg-main);
66
- --cx-icon-color: var(--cx-fg-color);
67
- --cx-border-color: var(--cx-border-main);
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
- flex-shrink: 0;
74
- color: var(--cx-fg-subtle);
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
- display: none;
78
+ .cxd-search-trigger-label,
79
+ .cxd-search-trigger-shortcut {
80
+ display: none;
80
81
 
81
- @include media-breakpoint-up(large) {
82
- display: inline-flex;
82
+ @include media-breakpoint-up(large) {
83
+ display: inline-flex;
84
+ }
83
85
  }
84
- }
85
86
 
86
- .cxd-search-trigger-label {
87
- flex: 1;
88
- overflow: hidden;
89
- text-overflow: ellipsis;
90
- white-space: nowrap;
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
- flex-shrink: 0;
95
- gap: .125rem;
96
- }
94
+ .cxd-search-trigger-shortcut {
95
+ flex-shrink: 0;
96
+ gap: .125rem;
97
+ }
97
98
 
98
- .cxd-search-trigger-key {
99
- display: inline-flex;
100
- align-items: center;
101
- justify-content: center;
102
- min-width: 1.25rem;
103
- padding: .125rem .375rem;
104
- font-family: var(--cx-font-family-code);
105
- @include font-size(var(--cx-font-size-small));
106
- @include line-height(var(--cx-line-height-small));
107
- color: var(--cx-fg-subtle);
108
- background-color: var(--cx-bg-evident);
109
- // border: var(--cx-border-width) solid var(--cx-border-color);
110
- @include border-radius(var(--cx-border-radius-small));
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
- .cxd-search-dialog {
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
- align-self: flex-start;
126
- width: 640px;
127
- max-height: min(80dvh, 720px);
128
- margin-block-start: 3rem;
129
- overflow: hidden;
130
- background-color: var(--cx-bg-even);
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
- @include media-breakpoint-down(small) {
133
- max-height: calc(100dvh - 4rem);
149
+ cxd-search-input {
150
+ flex-grow: 1;
134
151
  }
135
- }
136
152
 
137
- // Header / search form
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-form {
140
- display: flex;
141
- flex-wrap: nowrap;
142
- gap: .75rem;
143
- align-items: center;
144
- padding: var(--cx-space-medium);
145
- border-bottom: 0;
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-input {
149
- width: 100%;
150
- }
151
- // .cxd-search-input {
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
- .cxd-search-results {
178
- display: block;
179
- min-height: 6rem;
180
- border: 0;
181
- }
186
+ // Body / results
182
187
 
183
- .cxd-search-results-list {
184
- // --cx-list-border-width: 0;
185
- // --cx-list-border-radius: 0;
188
+ .cxd-search-body {
189
+ padding: 0 var(--cx-space-medium) var(--cx-space-medium);
190
+ }
186
191
 
187
- gap: var(--cx-space-small);
188
- padding-block-start: var(--cx-space-xsmall);
189
- list-style: none;
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-result {
197
- display: flex;
198
- flex-direction: column;
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
- // Default-active state: while focus is in the search input, the first result
205
- // is highlighted as the Enter target. Once the user arrows into the list,
206
- // :focus-within takes over.
207
- .cxd-search-results:not(:focus-within) .cxd-search-result:first-child > .cxd-search-item {
208
- @include focus-ring(true);
209
- --cx-focus-ring-offset: -2px;
210
- background-color: var(--cx-bg-main);
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
- &:has(+ .cxd-search-subresults) {
233
- border-block-end-width: 0;
234
- @include border-bottom-radius(0);
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
- &:hover {
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
- &:focus-visible {
242
- --cx-focus-ring-offset: -2px;
243
- z-index: 1;
244
- @include focus-ring(true);
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
- // &:not(.cxd-search-item-sub) {
248
- // border: var(--cx-border-width) solid var(--cx-border-color);
249
- // }
250
- }
243
+ mark {
244
+ background: none;
245
+ }
251
246
 
252
- .cxd-search-item-icon {
253
- // margin-block-start: .15rem;
254
- color: var(--cx-fg-subtle);
255
- }
247
+ &:has(+ .cxd-search-subresults) {
248
+ border-block-end-width: 0;
249
+ @include border-bottom-radius(0);
250
+ }
256
251
 
257
- .cxd-search-item-body {
258
- grid-column: 2;
259
- min-width: 0;
260
- }
252
+ &:hover {
253
+ background-color: var(--cx-bg-main);
254
+ }
261
255
 
262
- .cxd-search-item-title {
263
- font-weight: var(--cx-font-weight-strong);
264
- color: var(--cx-fg-main);
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-excerpt {
268
- overflow: hidden;
269
- color: var(--cx-fg-subtle);
270
- text-overflow: ellipsis;
271
- white-space: nowrap;
272
- @include font-size(var(--cx-font-size-small));
273
- @include line-height(var(--cx-line-height-small));
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-subitem {
277
- --cx-icon-size: var(--cx-icon-small);
278
- --cx-icon-color: var(--cx-fg-subtle);
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
- border-block-end-width: 0;
318
- @include border-radius(0);
319
- &:is(:last-child > .cxd-search-subitem) {
320
- border-block-end-width: var(--cx-border-width);
321
- @include border-bottom-radius(var(--cx-border-radius));
322
- &::before {
323
- height: 35%;
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
- // Recently visited
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
- .cxd-search-recent-header {
333
- display: flex;
334
- gap: .5rem;
335
- align-items: center;
336
- justify-content: space-between;
337
- padding-block: .5rem;
338
- }
342
+ }
343
+ }
339
344
 
340
- .cxd-search-recent-label {
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-clear {
349
- padding: .125rem .375rem;
350
- @include font-size(var(--cx-font-size-xsmall));
351
- color: var(--cx-fg-subtle);
352
- cursor: pointer;
353
- background-color: transparent;
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
- &:focus-visible {
364
- @include focus-ring(true);
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
- // Empty / loading / error states
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
- .cxd-search-empty,
371
- .cxd-search-error {
372
- padding: 2rem 1rem;
373
- @include font-size(var(--cx-font-size-small));
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
- .cxd-search-error {
379
- color: var(--cx-danger-fg-main);
380
- }
378
+ &:focus-visible {
379
+ @include focus-ring(true);
380
+ }
381
+ }
381
382
 
382
- .cxd-search-loading {
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-footer {
390
- padding-block: var(--cx-space-small);
391
- background-color: var(--cx-bg-evident);
392
- }
393
+ .cxd-search-error {
394
+ color: var(--cx-danger-fg-main);
395
+ }
393
396
 
394
- .cxd-search-shortcuts-list > .list-item {
395
- display: inline-flex;
396
- gap: .25rem;
397
- align-items: center;
398
- padding: 0 .5rem;
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-shortcut-label {
415
- padding-inline-start: .25rem;
416
- @include font-size(var(--cx-font-size-small));
417
- @include line-height(var(--cx-line-height-small));
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
  }
package/src/libs/vite.ts DELETED
@@ -1,45 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import type { Plugin } from 'vite'
4
-
5
- // Virtual module URL served by Vite's module graph at /@id/__x00__chassis-bundle.
6
- // Used to give the browser a single shared ESM instance of the chassis bundle in dev.
7
- const CHASSIS_BUNDLE_VIRTUAL_URL = '/@id/__x00__chassis-bundle'
8
-
9
- /**
10
- * Vite plugin that resolves `@chassis-ui/css` to a virtual module in dev so
11
- * both the `<script>` tag and any `import()` share one ESM instance.
12
- *
13
- * In build, `rollupOptions.external` + `paths` in the consuming `astro.config.ts`
14
- * handles deduplication instead.
15
- *
16
- * @param getChassisCSSFsPath - Returns the absolute path to the compiled Chassis CSS dist folder.
17
- */
18
- export function chassisBundlePlugin(getChassisCSSFsPath: () => string) {
19
- return {
20
- plugin: {
21
- name: 'chassis-ui-css',
22
- enforce: 'pre',
23
- apply: 'serve',
24
- resolveId(id: string) {
25
- if (id === '@chassis-ui/css') return '\0@chassis-ui/css'
26
- if (id === '/@chassis-bundle') return '\0chassis-bundle'
27
- },
28
- load(id: string) {
29
- if (id === '\0@chassis-ui/css') return `export * from "/@chassis-bundle"`
30
- if (id === '\0chassis-bundle') {
31
- const bundlePath = path.join(getChassisCSSFsPath(), 'js/chassis.bundle.js')
32
- const content = fs.readFileSync(bundlePath, 'utf-8')
33
- // Strip sourceMappingURL — the map isn't resolvable from a virtual module URL.
34
- return content.replace(/^\/\/# sourceMappingURL=.+$/m, '')
35
- }
36
- }
37
- } satisfies Plugin,
38
- // __CHASSIS_JS_SRC__ is a @chassis-ui/docs extension point: sets the URL for the
39
- // chassis JS <script> tag in dev mode. Points at the virtual module so the browser
40
- // gets one shared ESM instance instead of a second copy from public/.
41
- define: {
42
- 'globalThis.__CHASSIS_JS_SRC__': JSON.stringify(CHASSIS_BUNDLE_VIRTUAL_URL)
43
- }
44
- }
45
- }