@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.
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/jaad.css +1331 -0
- 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 +31 -0
- package/package.json +71 -3
- package/site.ts +36 -0
- package/src/config.ts +85 -0
- package/src/docs/advanced.ts +23 -0
- 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/docs/components/DocsPageNav.astro +53 -0
- package/src/docs/components/DocsSidebarLeft.astro +53 -0
- package/src/docs/components/PageActions.astro +83 -0
- package/src/docs/components/StructuredData.astro +55 -0
- package/src/docs/components/TableOfContents.astro +71 -0
- 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/docs/layouts/DefaultDocsFrame.astro +33 -0
- package/src/docs/layouts/Docs.astro +169 -0
- 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/docs/locale-tree.ts +51 -0
- 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/docs/routes/404.astro +16 -0
- package/src/docs/routes/docs-index.astro +19 -0
- package/src/docs/routes/docs-slug.astro +54 -0
- package/src/docs/routes/docs-slug.md.ts +35 -0
- package/src/docs/routes/llms-locale.txt.ts +8 -0
- package/src/docs/routes/llms.txt.ts +4 -0
- package/src/docs/routes/search-index-locale.json.ts +8 -0
- package/src/docs/routes/search-index.json.ts +4 -0
- 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-page.css +9 -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/docs/styles/jaamd.css +11 -0
- package/src/docs/types.ts +39 -0
- package/src/docs/urls.ts +35 -0
- package/src/env.d.ts +13 -0
- package/src/integration.ts +36 -0
- package/src/site/components/Footer.astro +18 -0
- package/src/site/components/Head.astro +119 -0
- package/src/site/components/Header.astro +50 -0
- package/src/site/components/LocaleSwitcher.astro +111 -0
- package/src/site/components/Nav.astro +22 -0
- package/src/site/components/NavMobile.astro +105 -0
- package/src/site/components/SearchPalette.astro +409 -0
- package/src/site/components/SocialLinks.astro +27 -0
- package/src/site/components/ThemeSwitcher.astro +55 -0
- package/src/site/components/locale-switcher.ts +6 -0
- package/src/site/config.ts +149 -0
- package/src/site/fonts/OFL.txt +94 -0
- package/src/site/fonts/inter.woff2 +0 -0
- package/src/site/fonts/merriweather-italic.woff2 +0 -0
- package/src/site/fonts/merriweather.woff2 +0 -0
- package/src/site/fonts.ts +29 -0
- package/src/site/forges.ts +60 -0
- package/src/site/helpers.ts +4 -0
- package/src/site/infer.ts +75 -0
- package/src/site/integration.ts +59 -0
- package/src/site/layouts/Shell.astro +41 -0
- package/src/site/locales.ts +199 -0
- package/src/site/runtime.ts +19 -0
- package/src/site/search.ts +227 -0
- package/src/site/social.ts +44 -0
- package/src/site/styles/components/footer.css +8 -0
- package/src/site/styles/components/header.css +31 -0
- package/src/site/styles/components/locale-switcher.css +20 -0
- package/src/site/styles/components/nav-mobile.css +15 -0
- package/src/site/styles/components/nav.css +11 -0
- package/src/site/styles/components/not-found.css +11 -0
- package/src/site/styles/components/search.css +118 -0
- package/src/site/styles/components/shell.css +77 -0
- package/src/site/styles/components/social-links.css +8 -0
- package/src/site/styles/components/theme-switcher.css +14 -0
- package/src/site/styles/components.css +10 -0
- package/src/site/styles/global.css +140 -0
- package/src/site/styles/jaamd-forward.css +16 -0
- package/src/site/styles/jaamd-reverse.css +18 -0
- package/src/site/styles/tailwind.css +30 -0
- package/src/site/themes.ts +26 -0
- package/src/site/ui/en.ts +27 -0
- package/src/site/ui/it.ts +28 -0
- package/src/site/ui.ts +49 -0
- package/src/site/urls.ts +14 -0
- package/src/site/virtual.ts +42 -0
- 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
|
+
}
|