@jaamd/jaad 0.11.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/jaad.css +232 -347
- package/docs/00-overview.md +25 -0
- package/docs/01-getting-started/01-installation.md +216 -0
- package/docs/01-getting-started/02-pages.md +109 -0
- package/docs/01-getting-started/03-deployment.md +100 -0
- package/docs/02-configurations/01-reference.md +215 -0
- package/docs/02-configurations/02-frontmatter.md +105 -0
- package/docs/02-configurations/03-i18n.md +210 -0
- package/docs/02-configurations/04-search.md +58 -0
- package/docs/02-configurations/05-themes.md +104 -0
- package/docs/02-configurations/06-styles.md +156 -0
- package/docs/02-configurations/07-custom-pages.md +119 -0
- package/docs/02-configurations/08-advanced-layout.md +139 -0
- package/docs/03-upgrading.md +208 -0
- package/index.ts +8 -22
- package/package.json +10 -9
- package/site.ts +18 -1
- package/src/config.ts +25 -176
- package/src/{advanced.ts → docs/advanced.ts} +6 -4
- package/src/docs/collection.ts +109 -0
- package/src/docs/components/Breadcrumbs.astro +29 -0
- package/src/docs/components/DocsMobileNav.astro +90 -0
- package/src/docs/components/DocsNavTree.astro +57 -0
- package/src/docs/components/DocsPage.astro +66 -0
- package/src/{components/docs → docs/components}/DocsPageNav.astro +4 -4
- package/src/docs/components/DocsSidebarLeft.astro +53 -0
- package/src/{components/docs → docs/components}/PageActions.astro +10 -8
- package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
- package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
- package/src/docs/config.ts +167 -0
- package/src/docs/content.ts +36 -0
- package/src/docs/endpoints.ts +89 -0
- package/src/docs/integration.ts +249 -0
- package/src/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
- package/src/{layouts → docs/layouts}/Docs.astro +44 -43
- package/src/docs/layouts/Page.astro +18 -0
- package/src/docs/link-plugins.ts +52 -0
- package/src/docs/links.ts +42 -0
- package/src/{locale-tree.ts → docs/locale-tree.ts} +1 -1
- package/src/docs/model/ids.ts +87 -0
- package/src/docs/model/rendered.ts +185 -0
- package/src/docs/model/tree.ts +124 -0
- package/src/docs/model/validate.ts +75 -0
- package/src/docs/navigation.ts +75 -0
- package/src/{routes → docs/routes}/404.astro +1 -1
- package/src/docs/routes/docs-index.astro +19 -0
- package/src/docs/routes/docs-slug.astro +54 -0
- package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
- package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
- package/src/{routes → docs/routes}/llms.txt.ts +1 -1
- package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
- package/src/{routes → docs/routes}/search-index.json.ts +1 -1
- package/src/docs/runtime.ts +17 -0
- package/src/docs/search-index.ts +39 -0
- package/src/docs/styles/components/breadcrumbs.css +26 -0
- package/src/docs/styles/components/docs-mobile-nav.css +47 -0
- package/src/docs/styles/components/docs-page-nav.css +41 -0
- package/src/docs/styles/components/docs-sidebar.css +59 -0
- package/src/docs/styles/components/page-actions.css +51 -0
- package/src/docs/styles/components/toc.css +38 -0
- package/src/docs/styles/components.css +7 -0
- package/src/docs/styles/docs.css +81 -0
- package/src/{@types/docs.ts → docs/types.ts} +8 -16
- package/src/{urls.ts → docs/urls.ts} +2 -16
- package/src/env.d.ts +1 -1
- package/src/integration.ts +15 -249
- package/src/{components → site/components}/Footer.astro +1 -1
- package/src/{components → site/components}/Header.astro +14 -11
- package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
- package/src/{components → site/components}/NavMobile.astro +1 -1
- package/src/{components → site/components}/SearchPalette.astro +160 -79
- package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
- package/src/site/config.ts +149 -0
- package/src/{infer.ts → site/infer.ts} +3 -3
- package/src/site/integration.ts +59 -0
- package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
- package/src/{locales.ts → site/locales.ts} +9 -1
- package/src/{docs-config.ts → site/runtime.ts} +1 -12
- package/src/site/search.ts +227 -0
- package/src/{social.ts → site/social.ts} +2 -2
- package/src/site/styles/components/header.css +31 -0
- package/src/{styles → site/styles}/components/search.css +21 -1
- package/src/{styles → site/styles}/components/shell.css +4 -0
- package/src/site/styles/components.css +10 -0
- package/src/{styles → site/styles}/global.css +4 -26
- package/src/{themes/index.ts → site/themes.ts} +1 -1
- package/src/{ui → site/ui}/en.ts +3 -5
- package/src/{ui → site/ui}/it.ts +2 -4
- package/src/site/urls.ts +14 -0
- package/src/{virtual.ts → site/virtual.ts} +12 -18
- package/src/styles.css +5 -0
- package/src/collection.ts +0 -97
- package/src/components/ScrollToTop.astro +0 -43
- package/src/components/docs/Breadcrumbs.astro +0 -36
- package/src/components/docs/DocsMobileNav.astro +0 -93
- package/src/components/docs/DocsPage.astro +0 -68
- package/src/components/docs/DocsSidebarLeft.astro +0 -82
- package/src/content.ts +0 -42
- package/src/navigation.ts +0 -97
- package/src/routes/docs-index.astro +0 -19
- package/src/routes/docs-slug.astro +0 -58
- package/src/styles/components/breadcrumbs.css +0 -13
- package/src/styles/components/docs-mobile-nav.css +0 -9
- package/src/styles/components/docs-page-nav.css +0 -31
- package/src/styles/components/docs-sidebar.css +0 -42
- package/src/styles/components/header.css +0 -20
- package/src/styles/components/page-actions.css +0 -13
- package/src/styles/components/scroll-to-top.css +0 -11
- package/src/styles/components/toc.css +0 -21
- package/src/styles/docs.css +0 -112
- package/src/utils/docs.ts +0 -300
- package/src/utils/endpoints.ts +0 -80
- package/src/utils/search.ts +0 -83
- /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
- /package/src/{styles → docs/styles}/jaamd.css +0 -0
- /package/src/{components → site/components}/Head.astro +0 -0
- /package/src/{components → site/components}/Nav.astro +0 -0
- /package/src/{components → site/components}/SocialLinks.astro +0 -0
- /package/src/{components → site/components}/locale-switcher.ts +0 -0
- /package/src/{fonts → site/fonts}/OFL.txt +0 -0
- /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
- /package/src/{fonts.ts → site/fonts.ts} +0 -0
- /package/src/{forges.ts → site/forges.ts} +0 -0
- /package/src/{utils → site}/helpers.ts +0 -0
- /package/src/{styles → site/styles}/components/footer.css +0 -0
- /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
- /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
- /package/src/{styles → site/styles}/components/nav.css +0 -0
- /package/src/{styles → site/styles}/components/not-found.css +0 -0
- /package/src/{styles → site/styles}/components/social-links.css +0 -0
- /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
- /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
- /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
- /package/src/{styles → site/styles}/tailwind.css +0 -0
- /package/src/{ui.ts → site/ui.ts} +0 -0
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { DEFAULT_LOCALE,
|
|
3
|
-
import { docsPageHref } from '../urls.ts'
|
|
2
|
+
import { DEFAULT_LOCALE, ui } from '../runtime.ts'
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/** The search index, of the reader's locale. */
|
|
6
|
+
indexUrl: string
|
|
7
|
+
/** The full field on wide screens. Otherwise the palette keeps its icon
|
|
8
|
+
* trigger and its shortcut. */
|
|
9
|
+
prominent?: boolean
|
|
10
10
|
locale?: string
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const {
|
|
14
|
-
const urls = docsUrls(locale)
|
|
13
|
+
const { indexUrl, prominent = false, locale = DEFAULT_LOCALE } = Astro.props
|
|
15
14
|
const t = ui(locale)
|
|
16
|
-
const base = docsPageHref('', urls)
|
|
17
|
-
const indexUrl = docsPageHref('search-index.json', urls)
|
|
18
15
|
|
|
19
16
|
const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
20
17
|
---
|
|
21
18
|
|
|
22
19
|
<jaad-search
|
|
23
|
-
data-
|
|
24
|
-
data-base={base}
|
|
20
|
+
data-prominent={prominent ? '' : null}
|
|
25
21
|
data-index-url={indexUrl}
|
|
26
22
|
data-loading={t('search.loading')}
|
|
27
23
|
data-empty={t('search.empty')}
|
|
28
24
|
>
|
|
29
|
-
<button data-trigger-desktop hidden={!
|
|
25
|
+
<button data-trigger-desktop hidden={!prominent} class="jaad-search-trigger" aria-label={t('search.label')}>
|
|
30
26
|
<svg class="jaad-search-trigger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
31
27
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
|
|
32
28
|
</svg>
|
|
@@ -38,7 +34,7 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
38
34
|
|
|
39
35
|
<button
|
|
40
36
|
data-trigger-icon
|
|
41
|
-
class:list={['jaad-icon-button',
|
|
37
|
+
class:list={['jaad-icon-button', prominent && 'jaad-search-trigger-compact']}
|
|
42
38
|
aria-label={t('search.label')}
|
|
43
39
|
>
|
|
44
40
|
<svg class="jaad-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
@@ -78,6 +74,12 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
78
74
|
</kbd>
|
|
79
75
|
</div>
|
|
80
76
|
|
|
77
|
+
<div data-filters hidden class="jaad-search-filters" role="group" aria-label={t('search.sections')}>
|
|
78
|
+
<button type="button" data-filter="" aria-pressed="true" class="jaad-search-filter">
|
|
79
|
+
{t('search.all')}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
81
83
|
<ul
|
|
82
84
|
data-results
|
|
83
85
|
id="search-results"
|
|
@@ -106,16 +108,30 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
106
108
|
|
|
107
109
|
<script>
|
|
108
110
|
import { navigate } from 'astro:transitions/client'
|
|
109
|
-
import {
|
|
111
|
+
import {
|
|
112
|
+
getSnippet,
|
|
113
|
+
markWords,
|
|
114
|
+
openingPages,
|
|
115
|
+
queryTerms,
|
|
116
|
+
search,
|
|
117
|
+
type SearchIndex,
|
|
118
|
+
type SearchPage,
|
|
119
|
+
} from '../search.ts'
|
|
120
|
+
|
|
121
|
+
const EMPTY_INDEX: SearchIndex = { sections: [], pages: [], entries: [] }
|
|
110
122
|
|
|
111
123
|
class JaadSearch extends HTMLElement {
|
|
112
124
|
#overlay!: HTMLElement
|
|
113
125
|
#input!: HTMLInputElement
|
|
126
|
+
#filters!: HTMLElement
|
|
114
127
|
#list!: HTMLUListElement
|
|
115
|
-
#index:
|
|
116
|
-
#request: Promise<
|
|
117
|
-
|
|
128
|
+
#index: SearchIndex | null = null
|
|
129
|
+
#request: Promise<SearchIndex> | null = null
|
|
130
|
+
/** The selectable rows, in display order. */
|
|
131
|
+
#options: { li: HTMLLIElement; href: string }[] = []
|
|
118
132
|
#selected = 0
|
|
133
|
+
/** The section key results are limited to; empty for all. */
|
|
134
|
+
#section = ''
|
|
119
135
|
#restoreFocus: HTMLElement | null = null
|
|
120
136
|
#previousOverflow = ''
|
|
121
137
|
|
|
@@ -126,6 +142,7 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
126
142
|
connectedCallback() {
|
|
127
143
|
this.#overlay = this.querySelector('[data-overlay]')!
|
|
128
144
|
this.#input = this.querySelector('[data-input]')!
|
|
145
|
+
this.#filters = this.querySelector('[data-filters]')!
|
|
129
146
|
this.#list = this.querySelector('[data-results]')!
|
|
130
147
|
|
|
131
148
|
if (/Mac|iPod|iPhone|iPad/.test(navigator.userAgent)) {
|
|
@@ -141,6 +158,10 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
141
158
|
|
|
142
159
|
this.querySelector('[data-backdrop]')!.addEventListener('click', () => this.#setOpen(false))
|
|
143
160
|
this.querySelector('[data-close]')!.addEventListener('click', () => this.#setOpen(false))
|
|
161
|
+
this.#filters.addEventListener('click', (e) => {
|
|
162
|
+
const button = (e.target as Element).closest<HTMLButtonElement>('[data-filter]')
|
|
163
|
+
if (button) this.#filter(button.dataset.filter ?? '')
|
|
164
|
+
})
|
|
144
165
|
|
|
145
166
|
this.#input.addEventListener('input', () => this.#render())
|
|
146
167
|
this.#input.addEventListener('keydown', (e) => this.#onInputKey(e))
|
|
@@ -159,15 +180,13 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
159
180
|
const next = (event as Event & { newDocument: Document }).newDocument.querySelector<HTMLElement>(
|
|
160
181
|
'jaad-search',
|
|
161
182
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const indexUrl = next.dataset.indexUrl
|
|
183
|
+
const indexUrl = next?.dataset.indexUrl
|
|
165
184
|
if (indexUrl && indexUrl !== this.dataset.indexUrl) {
|
|
166
185
|
this.dataset.indexUrl = indexUrl
|
|
167
186
|
this.#index = null
|
|
168
187
|
this.#request = null
|
|
188
|
+
this.#section = ''
|
|
169
189
|
}
|
|
170
|
-
if (next.dataset.base) this.dataset.base = next.dataset.base
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
#onGlobalKey = (e: KeyboardEvent) => {
|
|
@@ -183,26 +202,30 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
183
202
|
#onInputKey(e: KeyboardEvent) {
|
|
184
203
|
if (e.key === 'ArrowDown') {
|
|
185
204
|
e.preventDefault()
|
|
186
|
-
this.#select(Math.min(this.#selected + 1, this.#
|
|
205
|
+
this.#select(Math.min(this.#selected + 1, this.#options.length - 1))
|
|
187
206
|
} else if (e.key === 'ArrowUp') {
|
|
188
207
|
e.preventDefault()
|
|
189
208
|
this.#select(Math.max(this.#selected - 1, 0))
|
|
190
|
-
} else if (e.key === 'Enter' && this.#
|
|
191
|
-
this.#go(this.#
|
|
209
|
+
} else if (e.key === 'Enter' && this.#options[this.#selected]) {
|
|
210
|
+
this.#go(this.#options[this.#selected].href)
|
|
192
211
|
}
|
|
193
212
|
}
|
|
194
213
|
|
|
195
|
-
#loadIndex(): Promise<
|
|
214
|
+
#loadIndex(): Promise<SearchIndex> {
|
|
196
215
|
if (this.#index) return Promise.resolve(this.#index)
|
|
197
216
|
if (!this.#request) {
|
|
198
217
|
const url = this.dataset.indexUrl ?? '/search-index.json'
|
|
199
218
|
this.#request = fetch(url)
|
|
200
|
-
.then((res) => (res.ok ? res.json() :
|
|
201
|
-
.then((data:
|
|
219
|
+
.then((res) => (res.ok ? res.json() : EMPTY_INDEX))
|
|
220
|
+
.then((data: SearchIndex) => {
|
|
221
|
+
this.#index = data
|
|
222
|
+
this.#renderFilters(data)
|
|
223
|
+
return data
|
|
224
|
+
})
|
|
202
225
|
.catch(() => {
|
|
203
226
|
// Drop the failed promise so a later attempt can retry.
|
|
204
227
|
this.#request = null
|
|
205
|
-
return
|
|
228
|
+
return EMPTY_INDEX
|
|
206
229
|
})
|
|
207
230
|
}
|
|
208
231
|
return this.#request
|
|
@@ -226,28 +249,47 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
226
249
|
}
|
|
227
250
|
}
|
|
228
251
|
|
|
229
|
-
#go(
|
|
252
|
+
#go(href: string) {
|
|
230
253
|
this.#setOpen(false)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
navigate(href)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** One button per section, after "All"; none with fewer than two. */
|
|
258
|
+
#renderFilters(index: SearchIndex) {
|
|
259
|
+
const [all] = this.#filters.querySelectorAll('[data-filter]')
|
|
260
|
+
this.#filters.replaceChildren(all)
|
|
261
|
+
this.#filters.hidden = index.sections.length < 2
|
|
262
|
+
for (const { key, label } of index.sections) {
|
|
263
|
+
const button = document.createElement('button')
|
|
264
|
+
button.type = 'button'
|
|
265
|
+
button.dataset.filter = key
|
|
266
|
+
button.className = 'jaad-search-filter'
|
|
267
|
+
button.textContent = label
|
|
268
|
+
this.#filters.append(button)
|
|
269
|
+
}
|
|
270
|
+
this.#filter(this.#section)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#filter(section: string) {
|
|
274
|
+
this.#section = section
|
|
275
|
+
for (const button of this.#filters.querySelectorAll<HTMLElement>('[data-filter]')) {
|
|
276
|
+
button.setAttribute('aria-pressed', String(button.dataset.filter === section))
|
|
277
|
+
}
|
|
278
|
+
this.#render()
|
|
279
|
+
this.#input.focus()
|
|
234
280
|
}
|
|
235
281
|
|
|
236
282
|
#select(i: number) {
|
|
237
283
|
this.#selected = i
|
|
238
|
-
this.#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
})
|
|
243
|
-
;(this.#list.children[i] as HTMLElement | undefined)?.scrollIntoView({
|
|
244
|
-
block: 'nearest',
|
|
245
|
-
})
|
|
284
|
+
const option = this.#options[i]
|
|
285
|
+
if (option) this.#input.setAttribute('aria-activedescendant', option.li.id)
|
|
286
|
+
this.#options.forEach(({ li }, n) => li.setAttribute('aria-selected', String(n === i)))
|
|
287
|
+
option?.li.scrollIntoView({ block: 'nearest' })
|
|
246
288
|
}
|
|
247
289
|
|
|
248
|
-
#marked(text: string,
|
|
290
|
+
#marked(text: string, terms: string[]): DocumentFragment {
|
|
249
291
|
const frag = document.createDocumentFragment()
|
|
250
|
-
for (const part of
|
|
292
|
+
for (const part of markWords(text, terms)) {
|
|
251
293
|
if (part.match) {
|
|
252
294
|
const mark = document.createElement('mark')
|
|
253
295
|
mark.className = 'jaad-search-mark'
|
|
@@ -261,8 +303,8 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
261
303
|
}
|
|
262
304
|
|
|
263
305
|
#render() {
|
|
264
|
-
const query = this.#input.value
|
|
265
306
|
this.#list.replaceChildren()
|
|
307
|
+
this.#options = []
|
|
266
308
|
this.#input.removeAttribute('aria-activedescendant')
|
|
267
309
|
|
|
268
310
|
if (!this.#index) {
|
|
@@ -270,48 +312,87 @@ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
|
270
312
|
return
|
|
271
313
|
}
|
|
272
314
|
|
|
273
|
-
|
|
315
|
+
const query = this.#input.value
|
|
316
|
+
const terms = queryTerms(query)
|
|
317
|
+
const section = this.#section || undefined
|
|
318
|
+
|
|
319
|
+
if (terms.length === 0) {
|
|
320
|
+
for (const group of openingPages(this.#index, section)) {
|
|
321
|
+
if (group.label) this.#list.append(this.#label(group.label))
|
|
322
|
+
for (const page of group.pages) this.#option(page.href, this.#pageRow(page, [], ''))
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
for (const group of search(this.#index, query, section)) {
|
|
326
|
+
const snippet = group.intro ? getSnippet(group.intro.text, terms) : ''
|
|
327
|
+
this.#option(group.page.href, this.#pageRow(group.page, terms, snippet))
|
|
328
|
+
for (const hit of group.hits) {
|
|
329
|
+
const li = this.#option(hit.href, this.#row(hit.entry.heading ?? '', terms))
|
|
330
|
+
li.classList.add('jaad-search-result-heading')
|
|
331
|
+
this.#snippet(li, getSnippet(hit.entry.text, terms), terms)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
274
335
|
|
|
275
|
-
if (this.#
|
|
336
|
+
if (this.#options.length === 0) {
|
|
276
337
|
this.#list.append(this.#message(`${this.dataset.empty ?? 'No results for'} “${query}”`))
|
|
277
338
|
return
|
|
278
339
|
}
|
|
340
|
+
this.#select(0)
|
|
341
|
+
}
|
|
279
342
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (item.chapter) {
|
|
296
|
-
const badge = document.createElement('span')
|
|
297
|
-
badge.className = 'jaad-search-result-badge'
|
|
298
|
-
badge.textContent = item.chapter
|
|
299
|
-
head.append(badge)
|
|
300
|
-
}
|
|
301
|
-
li.append(head)
|
|
302
|
-
|
|
303
|
-
const snippet = getSnippet(item.body, query)
|
|
304
|
-
if (snippet) {
|
|
305
|
-
const p = document.createElement('p')
|
|
306
|
-
p.className = 'jaad-search-result-snippet'
|
|
307
|
-
p.append(this.#marked(snippet, query))
|
|
308
|
-
li.append(p)
|
|
309
|
-
}
|
|
343
|
+
/** A selectable row: the list item, given its content. */
|
|
344
|
+
#option(href: string, content: HTMLElement): HTMLLIElement {
|
|
345
|
+
const i = this.#options.length
|
|
346
|
+
const li = document.createElement('li')
|
|
347
|
+
li.id = `search-option-${i}`
|
|
348
|
+
li.setAttribute('role', 'option')
|
|
349
|
+
li.className = 'jaad-search-result'
|
|
350
|
+
li.addEventListener('click', () => this.#go(href))
|
|
351
|
+
li.addEventListener('mouseenter', () => this.#select(i))
|
|
352
|
+
li.append(content)
|
|
353
|
+
this.#list.append(li)
|
|
354
|
+
this.#options.push({ li, href })
|
|
355
|
+
return li
|
|
356
|
+
}
|
|
310
357
|
|
|
311
|
-
|
|
312
|
-
|
|
358
|
+
#row(title: string, terms: string[]): HTMLElement {
|
|
359
|
+
const head = document.createElement('div')
|
|
360
|
+
head.className = 'jaad-search-result-head'
|
|
361
|
+
const span = document.createElement('span')
|
|
362
|
+
span.className = 'jaad-search-result-title'
|
|
363
|
+
span.append(this.#marked(title, terms))
|
|
364
|
+
head.append(span)
|
|
365
|
+
return head
|
|
366
|
+
}
|
|
313
367
|
|
|
314
|
-
|
|
368
|
+
#pageRow(page: SearchPage, terms: string[], snippet: string): HTMLElement {
|
|
369
|
+
const fragment = document.createElement('div')
|
|
370
|
+
const head = this.#row(page.title, terms)
|
|
371
|
+
if (page.chapter) {
|
|
372
|
+
const badge = document.createElement('span')
|
|
373
|
+
badge.className = 'jaad-search-result-badge'
|
|
374
|
+
badge.textContent = page.chapter
|
|
375
|
+
head.append(badge)
|
|
376
|
+
}
|
|
377
|
+
fragment.append(head)
|
|
378
|
+
this.#snippet(fragment, snippet, terms)
|
|
379
|
+
return fragment
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#snippet(parent: HTMLElement, text: string, terms: string[]) {
|
|
383
|
+
if (!text) return
|
|
384
|
+
const p = document.createElement('p')
|
|
385
|
+
p.className = 'jaad-search-result-snippet'
|
|
386
|
+
p.append(this.#marked(text, terms))
|
|
387
|
+
parent.append(p)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#label(text: string): HTMLLIElement {
|
|
391
|
+
const li = document.createElement('li')
|
|
392
|
+
li.setAttribute('role', 'presentation')
|
|
393
|
+
li.className = 'jaad-search-group'
|
|
394
|
+
li.textContent = text
|
|
395
|
+
return li
|
|
315
396
|
}
|
|
316
397
|
|
|
317
398
|
#message(text: string): HTMLLIElement {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Locale } from './locales.ts'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { z } from 'astro/zod'
|
|
7
|
+
import { inferDescription, type RepoInfo } from './infer.ts'
|
|
8
|
+
import { PRESETS, PRESET_NAMES, isPreset } from './themes.ts'
|
|
9
|
+
|
|
10
|
+
const publicUrl = (field: string, example: string) =>
|
|
11
|
+
z.string().refine((v) => v.startsWith('/') || /^https?:\/\//.test(v), {
|
|
12
|
+
message: `${field} must be a public url, such as "${example}"`,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** The options of the site around the pages: identity, header, footer, head and theme. */
|
|
16
|
+
export const siteConfigShape = {
|
|
17
|
+
title: z.string(),
|
|
18
|
+
description: z.string().optional(),
|
|
19
|
+
lang: z.string().default('en'),
|
|
20
|
+
logo: publicUrl('logo', '/logo.svg').optional(),
|
|
21
|
+
/** Where the site name in the header leads: the site root, or another site. */
|
|
22
|
+
homeLink: z.string().default('/'),
|
|
23
|
+
|
|
24
|
+
social: z
|
|
25
|
+
.record(
|
|
26
|
+
z.string(),
|
|
27
|
+
z.union([
|
|
28
|
+
z.string(),
|
|
29
|
+
z.object({
|
|
30
|
+
href: z.string(),
|
|
31
|
+
label: z.string().optional(),
|
|
32
|
+
svg: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
]),
|
|
35
|
+
)
|
|
36
|
+
.default({}),
|
|
37
|
+
nav: z.array(z.object({ label: z.string(), href: z.string() })).default([]),
|
|
38
|
+
footer: z
|
|
39
|
+
.union([z.string(), z.literal(false)], {
|
|
40
|
+
error:
|
|
41
|
+
'footer is one line of text, or false to remove it. ' +
|
|
42
|
+
'The message/copyright pair is gone: write them as a single string, ' +
|
|
43
|
+
'e.g. "MIT Licensed · © 2026 Me · :credit".',
|
|
44
|
+
})
|
|
45
|
+
.optional(),
|
|
46
|
+
|
|
47
|
+
head: z
|
|
48
|
+
.array(
|
|
49
|
+
z.object({
|
|
50
|
+
tag: z.string(),
|
|
51
|
+
attrs: z.record(z.string(), z.union([z.string(), z.boolean()])).optional(),
|
|
52
|
+
content: z.string().optional(),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
.default([]),
|
|
56
|
+
ogImage: z.union([publicUrl('ogImage', '/og-image.png'), z.literal(false)]).optional(),
|
|
57
|
+
|
|
58
|
+
theme: z
|
|
59
|
+
.union([
|
|
60
|
+
z.string().refine(isPreset, {
|
|
61
|
+
message: `unknown theme; available: ${PRESET_NAMES.join(', ')}`,
|
|
62
|
+
}),
|
|
63
|
+
z.object({ light: z.string(), dark: z.string() }),
|
|
64
|
+
])
|
|
65
|
+
.default('default'),
|
|
66
|
+
|
|
67
|
+
appearance: z
|
|
68
|
+
.enum(['auto', 'light', 'dark'], {
|
|
69
|
+
error: 'unknown appearance; available: auto, light, dark',
|
|
70
|
+
})
|
|
71
|
+
.default('auto'),
|
|
72
|
+
|
|
73
|
+
/** Registers Tailwind (the site's own install) and exposes JAAD's tokens to it. */
|
|
74
|
+
tailwind: z.boolean().default(false),
|
|
75
|
+
|
|
76
|
+
/** Per-locale overrides for JAAD's bundled interface strings. */
|
|
77
|
+
ui: z.record(z.string(), z.record(z.string(), z.string())).default({}),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const siteConfigSchema = z.object(siteConfigShape)
|
|
81
|
+
|
|
82
|
+
export type SiteConfig = z.output<typeof siteConfigSchema>
|
|
83
|
+
|
|
84
|
+
/** The site options plus what the project could tell. Inferred fields are null
|
|
85
|
+
* when unavailable. */
|
|
86
|
+
export interface SiteResolvedConfig extends SiteConfig {
|
|
87
|
+
/** What jaamd gets for code blocks. */
|
|
88
|
+
shiki: string | { light: string; dark: string }
|
|
89
|
+
repoUrl: string | null
|
|
90
|
+
favicon: string | null
|
|
91
|
+
/** Explicit, inferred from public/, or false when no valid image exists. */
|
|
92
|
+
ogImage: string | false
|
|
93
|
+
/** Empty when the site is not localised. */
|
|
94
|
+
locales: Locale[]
|
|
95
|
+
/** `lang` when the site is not localised. */
|
|
96
|
+
defaultLocale: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const FAVICONS = ['favicon.svg', 'favicon.ico', 'favicon.png']
|
|
100
|
+
const OG_IMAGES = ['og-image.png', 'og-image.jpg', 'og-image.jpeg', 'og-image.webp']
|
|
101
|
+
|
|
102
|
+
function findPublicAsset(cwd: string, candidates: string[]): string | null {
|
|
103
|
+
for (const name of candidates) {
|
|
104
|
+
if (existsSync(join(cwd, 'public', name))) return `/${name}`
|
|
105
|
+
}
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The fields the site derives from its options and the project. */
|
|
110
|
+
export function resolveSiteConfig(
|
|
111
|
+
config: SiteConfig,
|
|
112
|
+
repo: RepoInfo | null,
|
|
113
|
+
cwd: string,
|
|
114
|
+
): Pick<SiteResolvedConfig, 'description' | 'repoUrl' | 'favicon' | 'ogImage' | 'shiki'> {
|
|
115
|
+
return {
|
|
116
|
+
description: config.description ?? inferDescription(cwd) ?? undefined,
|
|
117
|
+
repoUrl: repo?.url ?? null,
|
|
118
|
+
favicon: findPublicAsset(cwd, FAVICONS),
|
|
119
|
+
ogImage: config.ogImage ?? findPublicAsset(cwd, OG_IMAGES) ?? false,
|
|
120
|
+
shiki: typeof config.theme === 'string' ? PRESETS[config.theme].shiki : config.theme,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requireFrom = createRequire(import.meta.url)
|
|
125
|
+
|
|
126
|
+
const styleInPackage = (name: string) => fileURLToPath(new URL(`./styles/${name}`, import.meta.url))
|
|
127
|
+
|
|
128
|
+
/** Absolute paths, kept off the resolved config, which gets serialised. */
|
|
129
|
+
export function resolveStylesheets(
|
|
130
|
+
config: SiteConfig,
|
|
131
|
+
cwd: string = process.cwd(),
|
|
132
|
+
): {
|
|
133
|
+
user: string | null
|
|
134
|
+
theme: string | null
|
|
135
|
+
bridge: string
|
|
136
|
+
tailwind: string | null
|
|
137
|
+
} {
|
|
138
|
+
const userCss = join(cwd, 'src', 'jaad.css')
|
|
139
|
+
const slug = typeof config.theme === 'string' ? PRESETS[config.theme].theme : null
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
user: existsSync(userCss) ? userCss : null,
|
|
143
|
+
// Resolved through JAAMD's export map rather than by path.
|
|
144
|
+
theme: slug ? requireFrom.resolve(`@jaamd/astro/themes/${slug}.css`) : null,
|
|
145
|
+
// A preset supplies the palette, so the colours travel the other way.
|
|
146
|
+
bridge: styleInPackage(slug ? 'jaamd-reverse.css' : 'jaamd-forward.css'),
|
|
147
|
+
tailwind: config.tailwind ? styleInPackage('tailwind.css') : null,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
3
|
+
import { join, posix } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const git = (args: string[], cwd: string): string | null => {
|
|
6
6
|
try {
|
|
@@ -52,8 +52,8 @@ export function inferRepo(cwd: string = process.cwd()): RepoInfo | null {
|
|
|
52
52
|
/** Ends in a slash. `docsDir` resolves against the repository root, not the
|
|
53
53
|
* Astro project, so a site in a subdirectory still links correctly. */
|
|
54
54
|
export function editBaseFrom(repo: RepoInfo, docsDir: string): string {
|
|
55
|
-
|
|
56
|
-
const path = `${repo.prefix}${
|
|
55
|
+
// Normalised: a section can sit outside the project, as `../packages/x/docs`.
|
|
56
|
+
const path = posix.normalize(`${repo.prefix}${docsDir}`).replace(/^\/+|\/+$/g, '')
|
|
57
57
|
return `${repo.url}/edit/${repo.branch}/${path}/`
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SiteResolvedConfig } from './config.ts'
|
|
2
|
+
import type { SiteStylesheets } from './virtual.ts'
|
|
3
|
+
import type { AstroIntegration, AstroUserConfig } from 'astro'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { fonts } from './fonts.ts'
|
|
8
|
+
import { siteVirtualPlugin } from './virtual.ts'
|
|
9
|
+
|
|
10
|
+
type SetupParams = Parameters<NonNullable<AstroIntegration['hooks']['astro:config:setup']>>[0]
|
|
11
|
+
type VitePlugins = NonNullable<NonNullable<AstroUserConfig['vite']>['plugins']>
|
|
12
|
+
|
|
13
|
+
const NOT_INSTALLED = new Set(['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'])
|
|
14
|
+
|
|
15
|
+
/** The site's own `@tailwindcss/vite`, unless it already registered one. */
|
|
16
|
+
export function loadTailwindPlugin(root: string, registered: VitePlugins | undefined): VitePlugins {
|
|
17
|
+
const plugins = ([registered] as unknown[]).flat(Infinity) as ({
|
|
18
|
+
name?: string
|
|
19
|
+
} | null)[]
|
|
20
|
+
if (plugins.some((p) => p?.name?.startsWith('@tailwindcss/vite'))) return []
|
|
21
|
+
|
|
22
|
+
const load = createRequire(join(root, 'package.json'))
|
|
23
|
+
let tailwindcss: () => VitePlugins
|
|
24
|
+
try {
|
|
25
|
+
load.resolve('tailwindcss/package.json')
|
|
26
|
+
;({ default: tailwindcss } = load('@tailwindcss/vite') as {
|
|
27
|
+
default: () => VitePlugins
|
|
28
|
+
})
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!NOT_INSTALLED.has((error as { code?: string }).code ?? '')) throw error
|
|
31
|
+
throw new Error(
|
|
32
|
+
'jaad: `tailwind: true` needs Tailwind in your project:\n npm i -D tailwindcss @tailwindcss/vite',
|
|
33
|
+
{ cause: error },
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return tailwindcss()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The virtual modules, Tailwind when asked for, and the fonts. `config` is
|
|
40
|
+
* what `virtual:jaad/config` serves. */
|
|
41
|
+
export function setupSite(
|
|
42
|
+
{ config: astroConfig, updateConfig }: SetupParams,
|
|
43
|
+
config: SiteResolvedConfig,
|
|
44
|
+
stylesheets: SiteStylesheets,
|
|
45
|
+
): void {
|
|
46
|
+
updateConfig({
|
|
47
|
+
vite: {
|
|
48
|
+
plugins: [
|
|
49
|
+
siteVirtualPlugin(config, stylesheets),
|
|
50
|
+
...(config.tailwind
|
|
51
|
+
? [loadTailwindPlugin(fileURLToPath(astroConfig.root), astroConfig.vite.plugins)]
|
|
52
|
+
: []),
|
|
53
|
+
],
|
|
54
|
+
// Package .astro sources must reach the Astro compiler.
|
|
55
|
+
ssr: { noExternal: ['@jaamd/jaad'] },
|
|
56
|
+
},
|
|
57
|
+
fonts,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
2
3
|
import config from 'virtual:jaad/config'
|
|
3
4
|
import Footer from '../components/Footer.astro'
|
|
4
5
|
import Head from '../components/Head.astro'
|
|
5
6
|
import Header from '../components/Header.astro'
|
|
6
|
-
import '
|
|
7
|
+
import '../../../dist/jaad.css'
|
|
7
8
|
import 'virtual:jaad/theme.css'
|
|
8
9
|
import 'virtual:jaad/bridge.css'
|
|
9
10
|
import 'virtual:jaad/user.css'
|
|
@@ -15,9 +16,11 @@ interface Props {
|
|
|
15
16
|
bare?: boolean
|
|
16
17
|
/** Added to the body. */
|
|
17
18
|
class?: string
|
|
19
|
+
/** What the header offers: translations and search. */
|
|
20
|
+
header: ComponentProps<typeof Header>
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
const { title, description, bare = false, class: className } = Astro.props
|
|
23
|
+
const { title, description, bare = false, class: className, header } = Astro.props
|
|
21
24
|
|
|
22
25
|
const pageTitle = title ? `${title} | ${config.title}` : config.title
|
|
23
26
|
const pageDescription = description ?? config.description ?? config.title
|
|
@@ -29,7 +32,7 @@ const pageDescription = description ?? config.description ?? config.title
|
|
|
29
32
|
<Head title={pageTitle} description={pageDescription} />
|
|
30
33
|
</head>
|
|
31
34
|
<body class:list={['jaad-body', className]}>
|
|
32
|
-
<Header />
|
|
35
|
+
<Header {...header} />
|
|
33
36
|
<main class={bare ? 'jaad-main-bare' : 'jaad-main'}>
|
|
34
37
|
<slot />
|
|
35
38
|
</main>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ucfirst } from './
|
|
1
|
+
import { ucfirst } from './helpers.ts'
|
|
2
2
|
|
|
3
3
|
/** ISO 639-1. A three-letter directory such as `api` is never a locale. */
|
|
4
4
|
const LANGUAGES = new Set(
|
|
@@ -119,6 +119,14 @@ export interface Locale {
|
|
|
119
119
|
flag: string | null
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** Where the page a reader is on lives in one locale. */
|
|
123
|
+
export interface LocaleAlternate {
|
|
124
|
+
locale: Locale
|
|
125
|
+
href: string
|
|
126
|
+
/** False when this locale has no such page and the href is its opening one. */
|
|
127
|
+
translated: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
export function isLocale(segment: string): boolean {
|
|
123
131
|
const match = LOCALE_RE.exec(segment.toLowerCase())
|
|
124
132
|
return match !== null && LANGUAGES.has(match[1])
|