@jaamd/jaad 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +52 -2
- package/dist/jaad.css +1446 -0
- package/index.ts +45 -0
- package/package.json +70 -3
- package/site.ts +19 -0
- package/src/@types/docs.ts +47 -0
- package/src/advanced.ts +21 -0
- package/src/collection.ts +97 -0
- package/src/components/Footer.astro +18 -0
- package/src/components/Head.astro +119 -0
- package/src/components/Header.astro +47 -0
- package/src/components/LocaleSwitcher.astro +111 -0
- package/src/components/Nav.astro +22 -0
- package/src/components/NavMobile.astro +105 -0
- package/src/components/ScrollToTop.astro +43 -0
- package/src/components/SearchPalette.astro +328 -0
- package/src/components/SocialLinks.astro +27 -0
- package/src/components/ThemeSwitcher.astro +55 -0
- package/src/components/docs/Breadcrumbs.astro +36 -0
- package/src/components/docs/DocsMobileNav.astro +93 -0
- package/src/components/docs/DocsPage.astro +68 -0
- package/src/components/docs/DocsPageNav.astro +53 -0
- package/src/components/docs/DocsSidebarLeft.astro +82 -0
- package/src/components/docs/PageActions.astro +81 -0
- package/src/components/docs/StructuredData.astro +55 -0
- package/src/components/docs/TableOfContents.astro +73 -0
- package/src/components/locale-switcher.ts +6 -0
- package/src/config.ts +236 -0
- package/src/content.ts +42 -0
- package/src/docs-config.ts +30 -0
- package/src/env.d.ts +13 -0
- package/src/fonts/OFL.txt +94 -0
- package/src/fonts/inter.woff2 +0 -0
- package/src/fonts/merriweather-italic.woff2 +0 -0
- package/src/fonts/merriweather.woff2 +0 -0
- package/src/fonts.ts +29 -0
- package/src/forges.ts +60 -0
- package/src/infer.ts +75 -0
- package/src/integration.ts +270 -0
- package/src/layouts/DefaultDocsFrame.astro +31 -0
- package/src/layouts/Docs.astro +168 -0
- package/src/layouts/Page.astro +38 -0
- package/src/locale-tree.ts +51 -0
- package/src/locales.ts +191 -0
- package/src/navigation.ts +97 -0
- package/src/routes/404.astro +16 -0
- package/src/routes/docs-index.astro +19 -0
- package/src/routes/docs-slug.astro +58 -0
- package/src/routes/docs-slug.md.ts +37 -0
- package/src/routes/llms-locale.txt.ts +8 -0
- package/src/routes/llms.txt.ts +4 -0
- package/src/routes/search-index-locale.json.ts +8 -0
- package/src/routes/search-index.json.ts +4 -0
- package/src/social.ts +44 -0
- package/src/styles/components/breadcrumbs.css +13 -0
- package/src/styles/components/docs-mobile-nav.css +9 -0
- package/src/styles/components/docs-page-nav.css +31 -0
- package/src/styles/components/docs-page.css +9 -0
- package/src/styles/components/docs-sidebar.css +42 -0
- package/src/styles/components/footer.css +8 -0
- package/src/styles/components/header.css +20 -0
- package/src/styles/components/locale-switcher.css +20 -0
- package/src/styles/components/nav-mobile.css +15 -0
- package/src/styles/components/nav.css +11 -0
- package/src/styles/components/not-found.css +11 -0
- package/src/styles/components/page-actions.css +13 -0
- package/src/styles/components/scroll-to-top.css +11 -0
- package/src/styles/components/search.css +98 -0
- package/src/styles/components/shell.css +73 -0
- package/src/styles/components/social-links.css +8 -0
- package/src/styles/components/theme-switcher.css +14 -0
- package/src/styles/components/toc.css +21 -0
- package/src/styles/docs.css +112 -0
- package/src/styles/global.css +162 -0
- package/src/styles/jaamd-forward.css +16 -0
- package/src/styles/jaamd-reverse.css +18 -0
- package/src/styles/jaamd.css +11 -0
- package/src/styles/tailwind.css +30 -0
- package/src/themes/index.ts +26 -0
- package/src/ui/en.ts +29 -0
- package/src/ui/it.ts +30 -0
- package/src/ui.ts +49 -0
- package/src/urls.ts +49 -0
- package/src/utils/docs.ts +300 -0
- package/src/utils/endpoints.ts +80 -0
- package/src/utils/helpers.ts +4 -0
- package/src/utils/search.ts +83 -0
- package/src/virtual.ts +48 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { ui } from '../docs-config.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,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ui } from '../docs-config.ts'
|
|
3
|
+
|
|
4
|
+
const t = ui(Astro.props.locale)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<jaad-scroll-top>
|
|
8
|
+
<button class="jaad-scroll-top-button" aria-label={t('scrollTop')}>
|
|
9
|
+
<svg class="jaad-scroll-top-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
10
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
|
|
11
|
+
</svg>
|
|
12
|
+
</button>
|
|
13
|
+
</jaad-scroll-top>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
const SHOW_AFTER = 300
|
|
17
|
+
|
|
18
|
+
class JaadScrollTop extends HTMLElement {
|
|
19
|
+
#button: HTMLButtonElement | null = null
|
|
20
|
+
#onScroll = () => {
|
|
21
|
+
const shown = window.scrollY > SHOW_AFTER
|
|
22
|
+
this.#button?.toggleAttribute('data-visible', shown)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
connectedCallback() {
|
|
26
|
+
this.#button = this.querySelector('button')
|
|
27
|
+
this.#button?.addEventListener('click', () => {
|
|
28
|
+
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
29
|
+
window.scrollTo({ top: 0, behavior: reduced ? 'auto' : 'smooth' })
|
|
30
|
+
})
|
|
31
|
+
window.addEventListener('scroll', this.#onScroll, { passive: true })
|
|
32
|
+
this.#onScroll()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
window.removeEventListener('scroll', this.#onScroll)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!customElements.get('jaad-scroll-top')) {
|
|
41
|
+
customElements.define('jaad-scroll-top', JaadScrollTop)
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { DEFAULT_LOCALE, docsUrls, ui } from '../docs-config.ts'
|
|
3
|
+
import { docsPageHref } from '../urls.ts'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Whether this page is part of the docs. Off them the palette keeps its
|
|
7
|
+
* icon trigger and its shortcut, and searches the default locale. */
|
|
8
|
+
inDocs?: boolean
|
|
9
|
+
/** Search covers one locale: the one the reader is in. */
|
|
10
|
+
locale?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { inDocs = false, locale = DEFAULT_LOCALE } = Astro.props
|
|
14
|
+
const urls = docsUrls(locale)
|
|
15
|
+
const t = ui(locale)
|
|
16
|
+
const base = docsPageHref('', urls)
|
|
17
|
+
const indexUrl = docsPageHref('search-index.json', urls)
|
|
18
|
+
|
|
19
|
+
const MAGNIFIER = 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<jaad-search
|
|
23
|
+
data-in-docs={inDocs ? '' : null}
|
|
24
|
+
data-base={base}
|
|
25
|
+
data-index-url={indexUrl}
|
|
26
|
+
data-loading={t('search.loading')}
|
|
27
|
+
data-empty={t('search.empty')}
|
|
28
|
+
>
|
|
29
|
+
<button data-trigger-desktop hidden={!inDocs} class="jaad-search-trigger" aria-label={t('search.label')}>
|
|
30
|
+
<svg class="jaad-search-trigger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
31
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
|
|
32
|
+
</svg>
|
|
33
|
+
<span class="jaad-search-trigger-label">{t('search.trigger')}</span>
|
|
34
|
+
<kbd data-shortcut class="jaad-search-shortcut">
|
|
35
|
+
Ctrl K
|
|
36
|
+
</kbd>
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
<button
|
|
40
|
+
data-trigger-icon
|
|
41
|
+
class:list={['jaad-icon-button', inDocs && 'jaad-search-trigger-compact']}
|
|
42
|
+
aria-label={t('search.label')}
|
|
43
|
+
>
|
|
44
|
+
<svg class="jaad-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
45
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
|
|
49
|
+
<div data-overlay hidden class="jaad-search-overlay" role="dialog" aria-modal="true" aria-label={t('search.label')}>
|
|
50
|
+
<div data-backdrop class="jaad-search-backdrop"></div>
|
|
51
|
+
|
|
52
|
+
<div class="jaad-search-panel">
|
|
53
|
+
<div class="jaad-search-bar">
|
|
54
|
+
<svg
|
|
55
|
+
class="jaad-search-bar-icon"
|
|
56
|
+
fill="none"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
viewBox="0 0 24 24"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
>
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={MAGNIFIER}></path>
|
|
62
|
+
</svg>
|
|
63
|
+
<input
|
|
64
|
+
data-input
|
|
65
|
+
type="text"
|
|
66
|
+
placeholder={t('search.placeholder')}
|
|
67
|
+
class="jaad-search-input"
|
|
68
|
+
autocomplete="off"
|
|
69
|
+
spellcheck="false"
|
|
70
|
+
role="combobox"
|
|
71
|
+
aria-expanded="true"
|
|
72
|
+
aria-controls="search-results"
|
|
73
|
+
aria-autocomplete="list"
|
|
74
|
+
aria-label={t('search.label')}
|
|
75
|
+
/>
|
|
76
|
+
<kbd data-close class="jaad-search-close">
|
|
77
|
+
ESC
|
|
78
|
+
</kbd>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<ul
|
|
82
|
+
data-results
|
|
83
|
+
id="search-results"
|
|
84
|
+
class="jaad-search-results"
|
|
85
|
+
role="listbox"
|
|
86
|
+
aria-label={t('search.results')}
|
|
87
|
+
></ul>
|
|
88
|
+
|
|
89
|
+
<div class="jaad-search-footer">
|
|
90
|
+
<span>
|
|
91
|
+
<kbd class="jaad-search-key">↑↓</kbd>
|
|
92
|
+
{t('search.navigate')}
|
|
93
|
+
</span>
|
|
94
|
+
<span>
|
|
95
|
+
<kbd class="jaad-search-key">↵</kbd>
|
|
96
|
+
{t('search.open')}
|
|
97
|
+
</span>
|
|
98
|
+
<span>
|
|
99
|
+
<kbd class="jaad-search-key">esc</kbd>
|
|
100
|
+
{t('search.close')}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</jaad-search>
|
|
106
|
+
|
|
107
|
+
<script>
|
|
108
|
+
import { navigate } from 'astro:transitions/client'
|
|
109
|
+
import { scoreItems, getSnippet, splitOnMatches, type SearchItem } from '../utils/search.ts'
|
|
110
|
+
|
|
111
|
+
class JaadSearch extends HTMLElement {
|
|
112
|
+
#overlay!: HTMLElement
|
|
113
|
+
#input!: HTMLInputElement
|
|
114
|
+
#list!: HTMLUListElement
|
|
115
|
+
#index: SearchItem[] | null = null
|
|
116
|
+
#request: Promise<SearchItem[]> | null = null
|
|
117
|
+
#results: SearchItem[] = []
|
|
118
|
+
#selected = 0
|
|
119
|
+
#restoreFocus: HTMLElement | null = null
|
|
120
|
+
#previousOverflow = ''
|
|
121
|
+
|
|
122
|
+
get #open() {
|
|
123
|
+
return !this.#overlay.hidden
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
connectedCallback() {
|
|
127
|
+
this.#overlay = this.querySelector('[data-overlay]')!
|
|
128
|
+
this.#input = this.querySelector('[data-input]')!
|
|
129
|
+
this.#list = this.querySelector('[data-results]')!
|
|
130
|
+
|
|
131
|
+
if (/Mac|iPod|iPhone|iPad/.test(navigator.userAgent)) {
|
|
132
|
+
this.querySelector('[data-shortcut]')!.textContent = '⌘K'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const sel of ['[data-trigger-desktop]', '[data-trigger-icon]']) {
|
|
136
|
+
const trigger = this.querySelector<HTMLElement>(sel)!
|
|
137
|
+
trigger.addEventListener('click', () => this.#setOpen(true))
|
|
138
|
+
trigger.addEventListener('mouseenter', () => void this.#loadIndex())
|
|
139
|
+
trigger.addEventListener('focus', () => void this.#loadIndex())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.querySelector('[data-backdrop]')!.addEventListener('click', () => this.#setOpen(false))
|
|
143
|
+
this.querySelector('[data-close]')!.addEventListener('click', () => this.#setOpen(false))
|
|
144
|
+
|
|
145
|
+
this.#input.addEventListener('input', () => this.#render())
|
|
146
|
+
this.#input.addEventListener('keydown', (e) => this.#onInputKey(e))
|
|
147
|
+
|
|
148
|
+
document.addEventListener('keydown', this.#onGlobalKey)
|
|
149
|
+
document.addEventListener('astro:before-swap', this.#syncLocale)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
disconnectedCallback() {
|
|
153
|
+
document.removeEventListener('keydown', this.#onGlobalKey)
|
|
154
|
+
document.removeEventListener('astro:before-swap', this.#syncLocale)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Each locale has its own index, so a language switch drops the cache. */
|
|
158
|
+
#syncLocale = (event: Event) => {
|
|
159
|
+
const next = (event as Event & { newDocument: Document }).newDocument.querySelector<HTMLElement>(
|
|
160
|
+
'jaad-search',
|
|
161
|
+
)
|
|
162
|
+
if (!next) return
|
|
163
|
+
|
|
164
|
+
const indexUrl = next.dataset.indexUrl
|
|
165
|
+
if (indexUrl && indexUrl !== this.dataset.indexUrl) {
|
|
166
|
+
this.dataset.indexUrl = indexUrl
|
|
167
|
+
this.#index = null
|
|
168
|
+
this.#request = null
|
|
169
|
+
}
|
|
170
|
+
if (next.dataset.base) this.dataset.base = next.dataset.base
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#onGlobalKey = (e: KeyboardEvent) => {
|
|
174
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
175
|
+
e.preventDefault()
|
|
176
|
+
this.#setOpen(!this.#open)
|
|
177
|
+
} else if (e.key === 'Escape' && this.#open) {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
this.#setOpen(false)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#onInputKey(e: KeyboardEvent) {
|
|
184
|
+
if (e.key === 'ArrowDown') {
|
|
185
|
+
e.preventDefault()
|
|
186
|
+
this.#select(Math.min(this.#selected + 1, this.#results.length - 1))
|
|
187
|
+
} else if (e.key === 'ArrowUp') {
|
|
188
|
+
e.preventDefault()
|
|
189
|
+
this.#select(Math.max(this.#selected - 1, 0))
|
|
190
|
+
} else if (e.key === 'Enter' && this.#results[this.#selected]) {
|
|
191
|
+
this.#go(this.#results[this.#selected].slug)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#loadIndex(): Promise<SearchItem[]> {
|
|
196
|
+
if (this.#index) return Promise.resolve(this.#index)
|
|
197
|
+
if (!this.#request) {
|
|
198
|
+
const url = this.dataset.indexUrl ?? '/search-index.json'
|
|
199
|
+
this.#request = fetch(url)
|
|
200
|
+
.then((res) => (res.ok ? res.json() : []))
|
|
201
|
+
.then((data: SearchItem[]) => (this.#index = data))
|
|
202
|
+
.catch(() => {
|
|
203
|
+
// Drop the failed promise so a later attempt can retry.
|
|
204
|
+
this.#request = null
|
|
205
|
+
return []
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
return this.#request
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#setOpen(open: boolean) {
|
|
212
|
+
if (open === this.#open) return
|
|
213
|
+
this.#overlay.hidden = !open
|
|
214
|
+
|
|
215
|
+
if (open) {
|
|
216
|
+
this.#restoreFocus = document.activeElement as HTMLElement | null
|
|
217
|
+
this.#previousOverflow = document.body.style.overflow
|
|
218
|
+
document.body.style.overflow = 'hidden'
|
|
219
|
+
requestAnimationFrame(() => this.#input.focus())
|
|
220
|
+
void this.#loadIndex().then(() => this.#render())
|
|
221
|
+
this.#render()
|
|
222
|
+
} else {
|
|
223
|
+
document.body.style.overflow = this.#previousOverflow
|
|
224
|
+
this.#input.value = ''
|
|
225
|
+
this.#restoreFocus?.focus?.()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#go(slug: string) {
|
|
230
|
+
this.#setOpen(false)
|
|
231
|
+
const base = this.dataset.base ?? '/'
|
|
232
|
+
const prefix = base === '/' ? '' : base.replace(/\/$/, '')
|
|
233
|
+
navigate(slug ? `${prefix}/${slug}` : base)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#select(i: number) {
|
|
237
|
+
this.#selected = i
|
|
238
|
+
this.#input.setAttribute('aria-activedescendant', `search-option-${i}`)
|
|
239
|
+
;[...this.#list.children].forEach((li, n) => {
|
|
240
|
+
const active = n === i
|
|
241
|
+
li.setAttribute('aria-selected', String(active))
|
|
242
|
+
})
|
|
243
|
+
;(this.#list.children[i] as HTMLElement | undefined)?.scrollIntoView({
|
|
244
|
+
block: 'nearest',
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#marked(text: string, query: string): DocumentFragment {
|
|
249
|
+
const frag = document.createDocumentFragment()
|
|
250
|
+
for (const part of splitOnMatches(text, query)) {
|
|
251
|
+
if (part.match) {
|
|
252
|
+
const mark = document.createElement('mark')
|
|
253
|
+
mark.className = 'jaad-search-mark'
|
|
254
|
+
mark.textContent = part.text
|
|
255
|
+
frag.append(mark)
|
|
256
|
+
} else {
|
|
257
|
+
frag.append(document.createTextNode(part.text))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return frag
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#render() {
|
|
264
|
+
const query = this.#input.value
|
|
265
|
+
this.#list.replaceChildren()
|
|
266
|
+
this.#input.removeAttribute('aria-activedescendant')
|
|
267
|
+
|
|
268
|
+
if (!this.#index) {
|
|
269
|
+
this.#list.append(this.#message(this.dataset.loading ?? 'Loading...'))
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.#results = scoreItems(this.#index, query)
|
|
274
|
+
|
|
275
|
+
if (this.#results.length === 0) {
|
|
276
|
+
this.#list.append(this.#message(`${this.dataset.empty ?? 'No results for'} “${query}”`))
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.#results.forEach((item, i) => {
|
|
281
|
+
const li = document.createElement('li')
|
|
282
|
+
li.id = `search-option-${i}`
|
|
283
|
+
li.setAttribute('role', 'option')
|
|
284
|
+
li.className = 'jaad-search-result'
|
|
285
|
+
li.addEventListener('click', () => this.#go(item.slug))
|
|
286
|
+
li.addEventListener('mouseenter', () => this.#select(i))
|
|
287
|
+
|
|
288
|
+
const head = document.createElement('div')
|
|
289
|
+
head.className = 'jaad-search-result-head'
|
|
290
|
+
const title = document.createElement('span')
|
|
291
|
+
title.className = 'jaad-search-result-title'
|
|
292
|
+
title.append(this.#marked(item.title, query))
|
|
293
|
+
head.append(title)
|
|
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
|
+
}
|
|
310
|
+
|
|
311
|
+
this.#list.append(li)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
this.#select(0)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#message(text: string): HTMLLIElement {
|
|
318
|
+
const li = document.createElement('li')
|
|
319
|
+
li.className = 'jaad-search-message'
|
|
320
|
+
li.textContent = text
|
|
321
|
+
return li
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!customElements.get('jaad-search')) {
|
|
326
|
+
customElements.define('jaad-search', JaadSearch)
|
|
327
|
+
}
|
|
328
|
+
</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 '../docs-config.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,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { docsUrls, ui } from '../../docs-config.ts'
|
|
4
|
+
import { docsPageHref } from '../../urls.ts'
|
|
5
|
+
import { formatChapterTitle } from '../../utils/docs.ts'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
chapter?: string
|
|
9
|
+
title: string
|
|
10
|
+
locale?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { chapter, title, locale } = Astro.props
|
|
14
|
+
const t = ui(locale)
|
|
15
|
+
const chapterTitle = formatChapterTitle(chapter)
|
|
16
|
+
|
|
17
|
+
const rootSegment = config.docsBase.split('/').filter(Boolean).at(-1)
|
|
18
|
+
const rootLabel = formatChapterTitle(rootSegment) ?? t('breadcrumb.root')
|
|
19
|
+
const rootHref = docsPageHref('', docsUrls(locale))
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<nav aria-label={t('breadcrumb.label')} class="docs-breadcrumbs">
|
|
23
|
+
<a href={rootHref} class="docs-breadcrumbs-link">
|
|
24
|
+
{rootLabel}
|
|
25
|
+
</a>
|
|
26
|
+
{chapterTitle && (
|
|
27
|
+
<>
|
|
28
|
+
<span aria-hidden="true">/</span>
|
|
29
|
+
<span>{chapterTitle}</span>
|
|
30
|
+
</>
|
|
31
|
+
)}
|
|
32
|
+
<span aria-hidden="true">/</span>
|
|
33
|
+
<span class="docs-breadcrumbs-current" aria-current="page">
|
|
34
|
+
{title}
|
|
35
|
+
</span>
|
|
36
|
+
</nav>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsHeadings } from '../../@types/docs.ts'
|
|
3
|
+
import type { NavSection } from '../../navigation.ts'
|
|
4
|
+
import { ui } from '../../docs-config.ts'
|
|
5
|
+
import { formatChapterTitle } from '../../utils/docs.ts'
|
|
6
|
+
import ScrollToTop from '../ScrollToTop.astro'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
locale?: string
|
|
10
|
+
sections: NavSection[]
|
|
11
|
+
headings: DocsHeadings[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { sections, headings, locale } = Astro.props
|
|
15
|
+
const t = ui(locale)
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<div class="docs-mobile-nav">
|
|
19
|
+
<div class="docs-select">
|
|
20
|
+
<label for="page-select" class="jaad-sr-only">
|
|
21
|
+
{t('nav.selectPage')}
|
|
22
|
+
</label>
|
|
23
|
+
<select id="page-select" class="docs-select-field">
|
|
24
|
+
{sections.map((section) =>
|
|
25
|
+
section.type === 'page' ? (
|
|
26
|
+
<option value={section.item.href} selected={section.item.isActive}>
|
|
27
|
+
{section.item.title}
|
|
28
|
+
</option>
|
|
29
|
+
) : (
|
|
30
|
+
<optgroup label={formatChapterTitle(section.chapter) || section.chapter}>
|
|
31
|
+
{section.items.map((page) => (
|
|
32
|
+
<option value={page.href} selected={page.isActive}>
|
|
33
|
+
{page.title}
|
|
34
|
+
</option>
|
|
35
|
+
))}
|
|
36
|
+
</optgroup>
|
|
37
|
+
),
|
|
38
|
+
)}
|
|
39
|
+
</select>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{headings.length > 0 && (
|
|
43
|
+
<div id="section-select-wrapper" class="docs-select">
|
|
44
|
+
<label for="section-select" class="jaad-sr-only">
|
|
45
|
+
Jump to section
|
|
46
|
+
</label>
|
|
47
|
+
<select id="section-select" class="docs-select-field">
|
|
48
|
+
<option value="">{t('nav.jumpToSection')}</option>
|
|
49
|
+
{headings.map((heading) => {
|
|
50
|
+
const prefix = heading.depth === 3 ? '\u00A0\u00A0\u00A0\u00A0' : ''
|
|
51
|
+
return (
|
|
52
|
+
<option value={`#${heading.slug}`}>
|
|
53
|
+
{prefix}
|
|
54
|
+
{heading.text}
|
|
55
|
+
</option>
|
|
56
|
+
)
|
|
57
|
+
})}
|
|
58
|
+
</select>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<ScrollToTop {locale} />
|
|
64
|
+
|
|
65
|
+
<script>
|
|
66
|
+
import { navigate } from 'astro:transitions/client'
|
|
67
|
+
|
|
68
|
+
function initMobileNav(): void {
|
|
69
|
+
const pageSelect = document.querySelector<HTMLSelectElement>('#page-select')
|
|
70
|
+
const sectionSelect = document.querySelector<HTMLSelectElement>('#section-select')
|
|
71
|
+
|
|
72
|
+
if (pageSelect && !pageSelect.dataset.navInit) {
|
|
73
|
+
pageSelect.dataset.navInit = '1'
|
|
74
|
+
pageSelect.addEventListener('change', () => {
|
|
75
|
+
if (pageSelect.value) navigate(pageSelect.value)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (sectionSelect && !sectionSelect.dataset.navInit) {
|
|
80
|
+
sectionSelect.dataset.navInit = '1'
|
|
81
|
+
sectionSelect.addEventListener('change', () => {
|
|
82
|
+
const target = sectionSelect.value
|
|
83
|
+
if (!target) return
|
|
84
|
+
document.querySelector(target)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
85
|
+
// Reset so picking the same section twice in a row fires again.
|
|
86
|
+
setTimeout(() => (sectionSelect.value = ''), 500)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
initMobileNav()
|
|
92
|
+
document.addEventListener('astro:page-load', initMobileNav)
|
|
93
|
+
</script>
|