@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -2
  3. package/dist/jaad.css +1446 -0
  4. package/index.ts +45 -0
  5. package/package.json +70 -3
  6. package/site.ts +19 -0
  7. package/src/@types/docs.ts +47 -0
  8. package/src/advanced.ts +21 -0
  9. package/src/collection.ts +97 -0
  10. package/src/components/Footer.astro +18 -0
  11. package/src/components/Head.astro +119 -0
  12. package/src/components/Header.astro +47 -0
  13. package/src/components/LocaleSwitcher.astro +111 -0
  14. package/src/components/Nav.astro +22 -0
  15. package/src/components/NavMobile.astro +105 -0
  16. package/src/components/ScrollToTop.astro +43 -0
  17. package/src/components/SearchPalette.astro +328 -0
  18. package/src/components/SocialLinks.astro +27 -0
  19. package/src/components/ThemeSwitcher.astro +55 -0
  20. package/src/components/docs/Breadcrumbs.astro +36 -0
  21. package/src/components/docs/DocsMobileNav.astro +93 -0
  22. package/src/components/docs/DocsPage.astro +68 -0
  23. package/src/components/docs/DocsPageNav.astro +53 -0
  24. package/src/components/docs/DocsSidebarLeft.astro +82 -0
  25. package/src/components/docs/PageActions.astro +81 -0
  26. package/src/components/docs/StructuredData.astro +55 -0
  27. package/src/components/docs/TableOfContents.astro +73 -0
  28. package/src/components/locale-switcher.ts +6 -0
  29. package/src/config.ts +236 -0
  30. package/src/content.ts +42 -0
  31. package/src/docs-config.ts +30 -0
  32. package/src/env.d.ts +13 -0
  33. package/src/fonts/OFL.txt +94 -0
  34. package/src/fonts/inter.woff2 +0 -0
  35. package/src/fonts/merriweather-italic.woff2 +0 -0
  36. package/src/fonts/merriweather.woff2 +0 -0
  37. package/src/fonts.ts +29 -0
  38. package/src/forges.ts +60 -0
  39. package/src/infer.ts +75 -0
  40. package/src/integration.ts +270 -0
  41. package/src/layouts/DefaultDocsFrame.astro +31 -0
  42. package/src/layouts/Docs.astro +168 -0
  43. package/src/layouts/Page.astro +38 -0
  44. package/src/locale-tree.ts +51 -0
  45. package/src/locales.ts +191 -0
  46. package/src/navigation.ts +97 -0
  47. package/src/routes/404.astro +16 -0
  48. package/src/routes/docs-index.astro +19 -0
  49. package/src/routes/docs-slug.astro +58 -0
  50. package/src/routes/docs-slug.md.ts +37 -0
  51. package/src/routes/llms-locale.txt.ts +8 -0
  52. package/src/routes/llms.txt.ts +4 -0
  53. package/src/routes/search-index-locale.json.ts +8 -0
  54. package/src/routes/search-index.json.ts +4 -0
  55. package/src/social.ts +44 -0
  56. package/src/styles/components/breadcrumbs.css +13 -0
  57. package/src/styles/components/docs-mobile-nav.css +9 -0
  58. package/src/styles/components/docs-page-nav.css +31 -0
  59. package/src/styles/components/docs-page.css +9 -0
  60. package/src/styles/components/docs-sidebar.css +42 -0
  61. package/src/styles/components/footer.css +8 -0
  62. package/src/styles/components/header.css +20 -0
  63. package/src/styles/components/locale-switcher.css +20 -0
  64. package/src/styles/components/nav-mobile.css +15 -0
  65. package/src/styles/components/nav.css +11 -0
  66. package/src/styles/components/not-found.css +11 -0
  67. package/src/styles/components/page-actions.css +13 -0
  68. package/src/styles/components/scroll-to-top.css +11 -0
  69. package/src/styles/components/search.css +98 -0
  70. package/src/styles/components/shell.css +73 -0
  71. package/src/styles/components/social-links.css +8 -0
  72. package/src/styles/components/theme-switcher.css +14 -0
  73. package/src/styles/components/toc.css +21 -0
  74. package/src/styles/docs.css +112 -0
  75. package/src/styles/global.css +162 -0
  76. package/src/styles/jaamd-forward.css +16 -0
  77. package/src/styles/jaamd-reverse.css +18 -0
  78. package/src/styles/jaamd.css +11 -0
  79. package/src/styles/tailwind.css +30 -0
  80. package/src/themes/index.ts +26 -0
  81. package/src/ui/en.ts +29 -0
  82. package/src/ui/it.ts +30 -0
  83. package/src/ui.ts +49 -0
  84. package/src/urls.ts +49 -0
  85. package/src/utils/docs.ts +300 -0
  86. package/src/utils/endpoints.ts +80 -0
  87. package/src/utils/helpers.ts +4 -0
  88. package/src/utils/search.ts +83 -0
  89. 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>