@jaamd/jaad 0.0.0-stage → 1.0.0-rc.1

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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/jaad.css +1331 -0
  4. package/docs/00-overview.md +25 -0
  5. package/docs/01-getting-started/01-installation.md +216 -0
  6. package/docs/01-getting-started/02-pages.md +109 -0
  7. package/docs/01-getting-started/03-deployment.md +100 -0
  8. package/docs/02-configurations/01-reference.md +215 -0
  9. package/docs/02-configurations/02-frontmatter.md +105 -0
  10. package/docs/02-configurations/03-i18n.md +210 -0
  11. package/docs/02-configurations/04-search.md +58 -0
  12. package/docs/02-configurations/05-themes.md +104 -0
  13. package/docs/02-configurations/06-styles.md +156 -0
  14. package/docs/02-configurations/07-custom-pages.md +119 -0
  15. package/docs/02-configurations/08-advanced-layout.md +139 -0
  16. package/docs/03-upgrading.md +208 -0
  17. package/index.ts +31 -0
  18. package/package.json +71 -3
  19. package/site.ts +36 -0
  20. package/src/config.ts +85 -0
  21. package/src/docs/advanced.ts +23 -0
  22. package/src/docs/collection.ts +109 -0
  23. package/src/docs/components/Breadcrumbs.astro +29 -0
  24. package/src/docs/components/DocsMobileNav.astro +90 -0
  25. package/src/docs/components/DocsNavTree.astro +57 -0
  26. package/src/docs/components/DocsPage.astro +66 -0
  27. package/src/docs/components/DocsPageNav.astro +53 -0
  28. package/src/docs/components/DocsSidebarLeft.astro +53 -0
  29. package/src/docs/components/PageActions.astro +83 -0
  30. package/src/docs/components/StructuredData.astro +55 -0
  31. package/src/docs/components/TableOfContents.astro +71 -0
  32. package/src/docs/config.ts +167 -0
  33. package/src/docs/content.ts +36 -0
  34. package/src/docs/endpoints.ts +89 -0
  35. package/src/docs/integration.ts +249 -0
  36. package/src/docs/layouts/DefaultDocsFrame.astro +33 -0
  37. package/src/docs/layouts/Docs.astro +169 -0
  38. package/src/docs/layouts/Page.astro +18 -0
  39. package/src/docs/link-plugins.ts +52 -0
  40. package/src/docs/links.ts +42 -0
  41. package/src/docs/locale-tree.ts +51 -0
  42. package/src/docs/model/ids.ts +87 -0
  43. package/src/docs/model/rendered.ts +185 -0
  44. package/src/docs/model/tree.ts +124 -0
  45. package/src/docs/model/validate.ts +75 -0
  46. package/src/docs/navigation.ts +75 -0
  47. package/src/docs/routes/404.astro +16 -0
  48. package/src/docs/routes/docs-index.astro +19 -0
  49. package/src/docs/routes/docs-slug.astro +54 -0
  50. package/src/docs/routes/docs-slug.md.ts +35 -0
  51. package/src/docs/routes/llms-locale.txt.ts +8 -0
  52. package/src/docs/routes/llms.txt.ts +4 -0
  53. package/src/docs/routes/search-index-locale.json.ts +8 -0
  54. package/src/docs/routes/search-index.json.ts +4 -0
  55. package/src/docs/runtime.ts +17 -0
  56. package/src/docs/search-index.ts +39 -0
  57. package/src/docs/styles/components/breadcrumbs.css +26 -0
  58. package/src/docs/styles/components/docs-mobile-nav.css +47 -0
  59. package/src/docs/styles/components/docs-page-nav.css +41 -0
  60. package/src/docs/styles/components/docs-page.css +9 -0
  61. package/src/docs/styles/components/docs-sidebar.css +59 -0
  62. package/src/docs/styles/components/page-actions.css +51 -0
  63. package/src/docs/styles/components/toc.css +38 -0
  64. package/src/docs/styles/components.css +7 -0
  65. package/src/docs/styles/docs.css +81 -0
  66. package/src/docs/styles/jaamd.css +11 -0
  67. package/src/docs/types.ts +39 -0
  68. package/src/docs/urls.ts +35 -0
  69. package/src/env.d.ts +13 -0
  70. package/src/integration.ts +36 -0
  71. package/src/site/components/Footer.astro +18 -0
  72. package/src/site/components/Head.astro +119 -0
  73. package/src/site/components/Header.astro +50 -0
  74. package/src/site/components/LocaleSwitcher.astro +111 -0
  75. package/src/site/components/Nav.astro +22 -0
  76. package/src/site/components/NavMobile.astro +105 -0
  77. package/src/site/components/SearchPalette.astro +409 -0
  78. package/src/site/components/SocialLinks.astro +27 -0
  79. package/src/site/components/ThemeSwitcher.astro +55 -0
  80. package/src/site/components/locale-switcher.ts +6 -0
  81. package/src/site/config.ts +149 -0
  82. package/src/site/fonts/OFL.txt +94 -0
  83. package/src/site/fonts/inter.woff2 +0 -0
  84. package/src/site/fonts/merriweather-italic.woff2 +0 -0
  85. package/src/site/fonts/merriweather.woff2 +0 -0
  86. package/src/site/fonts.ts +29 -0
  87. package/src/site/forges.ts +60 -0
  88. package/src/site/helpers.ts +4 -0
  89. package/src/site/infer.ts +75 -0
  90. package/src/site/integration.ts +59 -0
  91. package/src/site/layouts/Shell.astro +41 -0
  92. package/src/site/locales.ts +199 -0
  93. package/src/site/runtime.ts +19 -0
  94. package/src/site/search.ts +227 -0
  95. package/src/site/social.ts +44 -0
  96. package/src/site/styles/components/footer.css +8 -0
  97. package/src/site/styles/components/header.css +31 -0
  98. package/src/site/styles/components/locale-switcher.css +20 -0
  99. package/src/site/styles/components/nav-mobile.css +15 -0
  100. package/src/site/styles/components/nav.css +11 -0
  101. package/src/site/styles/components/not-found.css +11 -0
  102. package/src/site/styles/components/search.css +118 -0
  103. package/src/site/styles/components/shell.css +77 -0
  104. package/src/site/styles/components/social-links.css +8 -0
  105. package/src/site/styles/components/theme-switcher.css +14 -0
  106. package/src/site/styles/components.css +10 -0
  107. package/src/site/styles/global.css +140 -0
  108. package/src/site/styles/jaamd-forward.css +16 -0
  109. package/src/site/styles/jaamd-reverse.css +18 -0
  110. package/src/site/styles/tailwind.css +30 -0
  111. package/src/site/themes.ts +26 -0
  112. package/src/site/ui/en.ts +27 -0
  113. package/src/site/ui/it.ts +28 -0
  114. package/src/site/ui.ts +49 -0
  115. package/src/site/urls.ts +14 -0
  116. package/src/site/virtual.ts +42 -0
  117. package/src/styles.css +5 -0
@@ -0,0 +1,105 @@
1
+ ---
2
+ import config from 'virtual:jaad/config'
3
+ import { ui } from '../runtime.ts'
4
+ import { withDeploymentBase } from '../urls.ts'
5
+
6
+ const links = config.nav
7
+ const t = ui(Astro.props.locale)
8
+ ---
9
+
10
+ {links.length > 0 && (
11
+ <jaad-nav-mobile class="jaad-nav-mobile">
12
+ <button
13
+ class="jaad-icon-button"
14
+ aria-label={t('nav.mobile')}
15
+ aria-expanded="false"
16
+ aria-haspopup="true"
17
+ aria-controls="jaad-nav-menu"
18
+ >
19
+ <svg class="jaad-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
20
+ <path
21
+ data-icon-open
22
+ stroke-linecap="round"
23
+ stroke-linejoin="round"
24
+ stroke-width="2"
25
+ d="M4 6h16M4 12h16M4 18h16"
26
+ />
27
+ <path
28
+ data-icon-close
29
+ stroke-linecap="round"
30
+ stroke-linejoin="round"
31
+ stroke-width="2"
32
+ d="M6 18L18 6M6 6l12 12"
33
+ />
34
+ </svg>
35
+ </button>
36
+
37
+ <div id="jaad-nav-menu" data-menu hidden class="jaad-menu">
38
+ <nav class="jaad-nav-mobile-links">
39
+ <ul class="jaad-nav-mobile-list">
40
+ {links.map((link) => (
41
+ <li>
42
+ <a
43
+ href={withDeploymentBase(link.href, import.meta.env.BASE_URL)}
44
+ class="jaad-menu-item"
45
+ {...(link.href.startsWith('http')
46
+ ? { target: '_blank', rel: 'noopener noreferrer' }
47
+ : {})}
48
+ >
49
+ {link.label}
50
+ </a>
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </nav>
55
+ </div>
56
+ </jaad-nav-mobile>
57
+ )}
58
+
59
+ <script>
60
+ class JaadNavMobile extends HTMLElement {
61
+ #button!: HTMLButtonElement
62
+ #menu!: HTMLElement
63
+
64
+ #onPointer = (e: PointerEvent) => {
65
+ if (!this.contains(e.target as Node)) this.#setOpen(false)
66
+ }
67
+ #onKey = (e: KeyboardEvent) => {
68
+ if (e.key === 'Escape') this.#setOpen(false)
69
+ }
70
+
71
+ #setOpen(open: boolean) {
72
+ this.#menu.hidden = !open
73
+ this.#button.setAttribute('aria-expanded', String(open))
74
+
75
+ if (open) {
76
+ document.addEventListener('pointerdown', this.#onPointer)
77
+ document.addEventListener('keydown', this.#onKey)
78
+ } else {
79
+ document.removeEventListener('pointerdown', this.#onPointer)
80
+ document.removeEventListener('keydown', this.#onKey)
81
+ }
82
+ }
83
+
84
+ connectedCallback() {
85
+ this.#button = this.querySelector('button')!
86
+ this.#menu = this.querySelector('[data-menu]')!
87
+
88
+ this.#button.addEventListener('click', () => {
89
+ this.#setOpen(Boolean(this.#menu.hidden))
90
+ })
91
+ this.#menu.addEventListener('click', (e) => {
92
+ if ((e.target as HTMLElement).closest('a')) this.#setOpen(false)
93
+ })
94
+ }
95
+
96
+ disconnectedCallback() {
97
+ document.removeEventListener('pointerdown', this.#onPointer)
98
+ document.removeEventListener('keydown', this.#onKey)
99
+ }
100
+ }
101
+
102
+ if (!customElements.get('jaad-nav-mobile')) {
103
+ customElements.define('jaad-nav-mobile', JaadNavMobile)
104
+ }
105
+ </script>
@@ -0,0 +1,409 @@
1
+ ---
2
+ import { DEFAULT_LOCALE, ui } from '../runtime.ts'
3
+
4
+ interface Props {
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
+ locale?: string
11
+ }
12
+
13
+ const { indexUrl, prominent = false, locale = DEFAULT_LOCALE } = Astro.props
14
+ const t = ui(locale)
15
+
16
+ const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
17
+ ---
18
+
19
+ <jaad-search
20
+ data-prominent={prominent ? '' : null}
21
+ data-index-url={indexUrl}
22
+ data-loading={t('search.loading')}
23
+ data-empty={t('search.empty')}
24
+ >
25
+ <button data-trigger-desktop hidden={!prominent} class="jaad-search-trigger" aria-label={t('search.label')}>
26
+ <svg class="jaad-search-trigger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
28
+ </svg>
29
+ <span class="jaad-search-trigger-label">{t('search.trigger')}</span>
30
+ <kbd data-shortcut class="jaad-search-shortcut">
31
+ Ctrl K
32
+ </kbd>
33
+ </button>
34
+
35
+ <button
36
+ data-trigger-icon
37
+ class:list={['jaad-icon-button', prominent && 'jaad-search-trigger-compact']}
38
+ aria-label={t('search.label')}
39
+ >
40
+ <svg class="jaad-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
42
+ </svg>
43
+ </button>
44
+
45
+ <div data-overlay hidden class="jaad-search-overlay" role="dialog" aria-modal="true" aria-label={t('search.label')}>
46
+ <div data-backdrop class="jaad-search-backdrop"></div>
47
+
48
+ <div class="jaad-search-panel">
49
+ <div class="jaad-search-bar">
50
+ <svg
51
+ class="jaad-search-bar-icon"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ viewBox="0 0 24 24"
55
+ aria-hidden="true"
56
+ >
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
58
+ </svg>
59
+ <input
60
+ data-input
61
+ type="text"
62
+ placeholder={t('search.placeholder')}
63
+ class="jaad-search-input"
64
+ autocomplete="off"
65
+ spellcheck="false"
66
+ role="combobox"
67
+ aria-expanded="true"
68
+ aria-controls="search-results"
69
+ aria-autocomplete="list"
70
+ aria-label={t('search.label')}
71
+ />
72
+ <kbd data-close class="jaad-search-close">
73
+ ESC
74
+ </kbd>
75
+ </div>
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
+
83
+ <ul
84
+ data-results
85
+ id="search-results"
86
+ class="jaad-search-results"
87
+ role="listbox"
88
+ aria-label={t('search.results')}
89
+ ></ul>
90
+
91
+ <div class="jaad-search-footer">
92
+ <span>
93
+ <kbd class="jaad-search-key">↑↓</kbd>
94
+ {t('search.navigate')}
95
+ </span>
96
+ <span>
97
+ <kbd class="jaad-search-key">↵</kbd>
98
+ {t('search.open')}
99
+ </span>
100
+ <span>
101
+ <kbd class="jaad-search-key">esc</kbd>
102
+ {t('search.close')}
103
+ </span>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </jaad-search>
108
+
109
+ <script>
110
+ import { navigate } from 'astro:transitions/client'
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: [] }
122
+
123
+ class JaadSearch extends HTMLElement {
124
+ #overlay!: HTMLElement
125
+ #input!: HTMLInputElement
126
+ #filters!: HTMLElement
127
+ #list!: HTMLUListElement
128
+ #index: SearchIndex | null = null
129
+ #request: Promise<SearchIndex> | null = null
130
+ /** The selectable rows, in display order. */
131
+ #options: { li: HTMLLIElement; href: string }[] = []
132
+ #selected = 0
133
+ /** The section key results are limited to; empty for all. */
134
+ #section = ''
135
+ #restoreFocus: HTMLElement | null = null
136
+ #previousOverflow = ''
137
+
138
+ get #open() {
139
+ return !this.#overlay.hidden
140
+ }
141
+
142
+ connectedCallback() {
143
+ this.#overlay = this.querySelector('[data-overlay]')!
144
+ this.#input = this.querySelector('[data-input]')!
145
+ this.#filters = this.querySelector('[data-filters]')!
146
+ this.#list = this.querySelector('[data-results]')!
147
+
148
+ if (/Mac|iPod|iPhone|iPad/.test(navigator.userAgent)) {
149
+ this.querySelector('[data-shortcut]')!.textContent = '⌘K'
150
+ }
151
+
152
+ for (const sel of ['[data-trigger-desktop]', '[data-trigger-icon]']) {
153
+ const trigger = this.querySelector<HTMLElement>(sel)!
154
+ trigger.addEventListener('click', () => this.#setOpen(true))
155
+ trigger.addEventListener('mouseenter', () => void this.#loadIndex())
156
+ trigger.addEventListener('focus', () => void this.#loadIndex())
157
+ }
158
+
159
+ this.querySelector('[data-backdrop]')!.addEventListener('click', () => this.#setOpen(false))
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
+ })
165
+
166
+ this.#input.addEventListener('input', () => this.#render())
167
+ this.#input.addEventListener('keydown', (e) => this.#onInputKey(e))
168
+
169
+ document.addEventListener('keydown', this.#onGlobalKey)
170
+ document.addEventListener('astro:before-swap', this.#syncLocale)
171
+ }
172
+
173
+ disconnectedCallback() {
174
+ document.removeEventListener('keydown', this.#onGlobalKey)
175
+ document.removeEventListener('astro:before-swap', this.#syncLocale)
176
+ }
177
+
178
+ /** Each locale has its own index, so a language switch drops the cache. */
179
+ #syncLocale = (event: Event) => {
180
+ const next = (event as Event & { newDocument: Document }).newDocument.querySelector<HTMLElement>(
181
+ 'jaad-search',
182
+ )
183
+ const indexUrl = next?.dataset.indexUrl
184
+ if (indexUrl && indexUrl !== this.dataset.indexUrl) {
185
+ this.dataset.indexUrl = indexUrl
186
+ this.#index = null
187
+ this.#request = null
188
+ this.#section = ''
189
+ }
190
+ }
191
+
192
+ #onGlobalKey = (e: KeyboardEvent) => {
193
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
194
+ e.preventDefault()
195
+ this.#setOpen(!this.#open)
196
+ } else if (e.key === 'Escape' && this.#open) {
197
+ e.preventDefault()
198
+ this.#setOpen(false)
199
+ }
200
+ }
201
+
202
+ #onInputKey(e: KeyboardEvent) {
203
+ if (e.key === 'ArrowDown') {
204
+ e.preventDefault()
205
+ this.#select(Math.min(this.#selected + 1, this.#options.length - 1))
206
+ } else if (e.key === 'ArrowUp') {
207
+ e.preventDefault()
208
+ this.#select(Math.max(this.#selected - 1, 0))
209
+ } else if (e.key === 'Enter' && this.#options[this.#selected]) {
210
+ this.#go(this.#options[this.#selected].href)
211
+ }
212
+ }
213
+
214
+ #loadIndex(): Promise<SearchIndex> {
215
+ if (this.#index) return Promise.resolve(this.#index)
216
+ if (!this.#request) {
217
+ const url = this.dataset.indexUrl ?? '/search-index.json'
218
+ this.#request = fetch(url)
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
+ })
225
+ .catch(() => {
226
+ // Drop the failed promise so a later attempt can retry.
227
+ this.#request = null
228
+ return EMPTY_INDEX
229
+ })
230
+ }
231
+ return this.#request
232
+ }
233
+
234
+ #setOpen(open: boolean) {
235
+ if (open === this.#open) return
236
+ this.#overlay.hidden = !open
237
+
238
+ if (open) {
239
+ this.#restoreFocus = document.activeElement as HTMLElement | null
240
+ this.#previousOverflow = document.body.style.overflow
241
+ document.body.style.overflow = 'hidden'
242
+ requestAnimationFrame(() => this.#input.focus())
243
+ void this.#loadIndex().then(() => this.#render())
244
+ this.#render()
245
+ } else {
246
+ document.body.style.overflow = this.#previousOverflow
247
+ this.#input.value = ''
248
+ this.#restoreFocus?.focus?.()
249
+ }
250
+ }
251
+
252
+ #go(href: string) {
253
+ this.#setOpen(false)
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()
280
+ }
281
+
282
+ #select(i: number) {
283
+ this.#selected = i
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' })
288
+ }
289
+
290
+ #marked(text: string, terms: string[]): DocumentFragment {
291
+ const frag = document.createDocumentFragment()
292
+ for (const part of markWords(text, terms)) {
293
+ if (part.match) {
294
+ const mark = document.createElement('mark')
295
+ mark.className = 'jaad-search-mark'
296
+ mark.textContent = part.text
297
+ frag.append(mark)
298
+ } else {
299
+ frag.append(document.createTextNode(part.text))
300
+ }
301
+ }
302
+ return frag
303
+ }
304
+
305
+ #render() {
306
+ this.#list.replaceChildren()
307
+ this.#options = []
308
+ this.#input.removeAttribute('aria-activedescendant')
309
+
310
+ if (!this.#index) {
311
+ this.#list.append(this.#message(this.dataset.loading ?? 'Loading...'))
312
+ return
313
+ }
314
+
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
+ }
335
+
336
+ if (this.#options.length === 0) {
337
+ this.#list.append(this.#message(`${this.dataset.empty ?? 'No results for'} “${query}”`))
338
+ return
339
+ }
340
+ this.#select(0)
341
+ }
342
+
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
+ }
357
+
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
+ }
367
+
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
396
+ }
397
+
398
+ #message(text: string): HTMLLIElement {
399
+ const li = document.createElement('li')
400
+ li.className = 'jaad-search-message'
401
+ li.textContent = text
402
+ return li
403
+ }
404
+ }
405
+
406
+ if (!customElements.get('jaad-search')) {
407
+ customElements.define('jaad-search', JaadSearch)
408
+ }
409
+ </script>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import config from 'virtual:jaad/config'
3
+ import { buildSocialLinks } from '../social.ts'
4
+
5
+ const links = buildSocialLinks(config)
6
+ ---
7
+
8
+ {links.map((link) => (
9
+ <a
10
+ href={link.href}
11
+ target="_blank"
12
+ rel="noopener noreferrer"
13
+ title={link.label}
14
+ aria-label={link.label}
15
+ class="jaad-icon-button"
16
+ >
17
+ {link.svg ? (
18
+ <span class="jaad-social-icon" set:html={link.svg} />
19
+ ) : link.path ? (
20
+ <svg class="jaad-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
21
+ <path d={link.path} />
22
+ </svg>
23
+ ) : (
24
+ <span class="jaad-social-label">{link.label}</span>
25
+ )}
26
+ </a>
27
+ ))}
@@ -0,0 +1,55 @@
1
+ ---
2
+ // The theme class and the icon are already set in <head>; only the click is JS.
3
+ import { ui } from '../runtime.ts'
4
+
5
+ const t = ui(Astro.props.locale)
6
+ ---
7
+
8
+ <jaad-theme-toggle>
9
+ <button class="jaad-icon-button" aria-label={t('theme.toggle')} title={t('theme.toggle')}>
10
+ <svg
11
+ class="jaad-theme-icon-to-dark jaad-icon"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ viewBox="0 0 24 24"
15
+ aria-hidden="true"
16
+ >
17
+ <path
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ stroke-width="2"
21
+ d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
22
+ ></path>
23
+ </svg>
24
+
25
+ <svg
26
+ class="jaad-theme-icon-to-light jaad-icon"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ viewBox="0 0 24 24"
30
+ aria-hidden="true"
31
+ >
32
+ <path
33
+ stroke-linecap="round"
34
+ stroke-linejoin="round"
35
+ stroke-width="2"
36
+ d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
37
+ ></path>
38
+ </svg>
39
+ </button>
40
+ </jaad-theme-toggle>
41
+
42
+ <script>
43
+ class JaadThemeToggle extends HTMLElement {
44
+ connectedCallback() {
45
+ this.querySelector('button')?.addEventListener('click', () => {
46
+ const isDark = document.documentElement.classList.toggle('dark')
47
+ localStorage.setItem('theme', isDark ? 'dark' : 'light')
48
+ })
49
+ }
50
+ }
51
+
52
+ if (!customElements.get('jaad-theme-toggle')) {
53
+ customElements.define('jaad-theme-toggle', JaadThemeToggle)
54
+ }
55
+ </script>
@@ -0,0 +1,6 @@
1
+ /** The markup the persisted switcher should adopt from an incoming document,
2
+ * or null when there is nothing to adopt. */
3
+ export function nextSwitcherMarkup(incoming: Document): string | null {
4
+ const next = incoming.querySelector('jaad-locale-switcher')
5
+ return next ? next.innerHTML : null
6
+ }