@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,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsEntry } from '../../@types/docs.ts'
|
|
3
|
+
import MarkdownContent from '@jaamd/jaamd/components'
|
|
4
|
+
import config from 'virtual:jaad/config'
|
|
5
|
+
import { DEFAULT_LOCALE, LOCALES, docsUrls, ui } from '../../docs-config.ts'
|
|
6
|
+
import DocsLayout from '../../layouts/Docs.astro'
|
|
7
|
+
import { buildDocsNavigation } from '../../navigation.ts'
|
|
8
|
+
import {
|
|
9
|
+
getCleanSlug,
|
|
10
|
+
parseDocCollectionId,
|
|
11
|
+
docTitle,
|
|
12
|
+
extractHeadingsFromMarkdown,
|
|
13
|
+
routedId,
|
|
14
|
+
} from '../../utils/docs.ts'
|
|
15
|
+
import Breadcrumbs from './Breadcrumbs.astro'
|
|
16
|
+
import DocsMobileNav from './DocsMobileNav.astro'
|
|
17
|
+
import DocsPageNav from './DocsPageNav.astro'
|
|
18
|
+
import PageActions from './PageActions.astro'
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
entry: DocsEntry
|
|
22
|
+
sortedPages: DocsEntry[]
|
|
23
|
+
/** Left out on the opening page, whose title is the site name alone. */
|
|
24
|
+
headTitle?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { entry, sortedPages, headTitle } = Astro.props
|
|
28
|
+
|
|
29
|
+
const cleanSlug = getCleanSlug(routedId(entry))
|
|
30
|
+
const { chapter } = parseDocCollectionId(routedId(entry))
|
|
31
|
+
const title = docTitle(entry)
|
|
32
|
+
const headings = extractHeadingsFromMarkdown(entry.body || '')
|
|
33
|
+
const urls = docsUrls(entry.locale)
|
|
34
|
+
const t = ui(entry.locale)
|
|
35
|
+
const language = LOCALES.find((known) => known.code === (entry.locale ?? DEFAULT_LOCALE))?.tag ?? config.lang
|
|
36
|
+
const updated = entry.data.lastUpdated
|
|
37
|
+
const navigation = buildDocsNavigation(sortedPages, entry.id, urls)
|
|
38
|
+
const contentAttrs = { id: 'content' }
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<DocsLayout title={headTitle} chapter={chapter} entry={entry} headings={headings} {navigation}>
|
|
42
|
+
<DocsMobileNav slot="mobile-navigation" sections={navigation.sections} headings={headings} locale={entry.locale} />
|
|
43
|
+
|
|
44
|
+
<Breadcrumbs slot="breadcrumbs" chapter={chapter} title={title} locale={entry.locale} />
|
|
45
|
+
<PageActions slot="page-actions" id={entry.id} cleanSlug={cleanSlug} locale={entry.locale} />
|
|
46
|
+
|
|
47
|
+
<Fragment slot="content">
|
|
48
|
+
<MarkdownContent {...contentAttrs} class="docs-content">
|
|
49
|
+
<slot />
|
|
50
|
+
</MarkdownContent>
|
|
51
|
+
|
|
52
|
+
{updated && (
|
|
53
|
+
<p class="docs-updated">
|
|
54
|
+
{t('page.lastUpdated')}{' '}
|
|
55
|
+
<time datetime={updated.toISOString()}>
|
|
56
|
+
{new Intl.DateTimeFormat(language, { dateStyle: 'long' }).format(updated)}
|
|
57
|
+
</time>
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
</Fragment>
|
|
61
|
+
|
|
62
|
+
<DocsPageNav
|
|
63
|
+
slot="page-navigation"
|
|
64
|
+
previousPage={navigation.previousPage}
|
|
65
|
+
nextPage={navigation.nextPage}
|
|
66
|
+
locale={entry.locale}
|
|
67
|
+
/>
|
|
68
|
+
</DocsLayout>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsNavItem } from '../../@types/docs.ts'
|
|
3
|
+
import { ui } from '../../docs-config.ts'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string
|
|
7
|
+
previousPage: DocsNavItem | null
|
|
8
|
+
nextPage: DocsNavItem | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { previousPage, nextPage, locale } = Astro.props
|
|
12
|
+
const t = ui(locale)
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{(previousPage || nextPage) && (
|
|
16
|
+
<nav class="docs-page-nav" aria-label={t('nav.page')}>
|
|
17
|
+
<div class="docs-page-nav-row">
|
|
18
|
+
{previousPage && previousPage.title ? (
|
|
19
|
+
<a href={previousPage.href} rel="prev" class="docs-page-nav-link">
|
|
20
|
+
<svg
|
|
21
|
+
class="docs-page-nav-icon docs-page-nav-icon-prev"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
viewBox="0 0 24 24"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
>
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
28
|
+
</svg>
|
|
29
|
+
<span class="docs-page-nav-label">{previousPage.title}</span>
|
|
30
|
+
</a>
|
|
31
|
+
) : (
|
|
32
|
+
<div />
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
{nextPage && nextPage.title ? (
|
|
36
|
+
<a href={nextPage.href} rel="next" class="docs-page-nav-link">
|
|
37
|
+
<span class="docs-page-nav-label">{nextPage.title}</span>
|
|
38
|
+
<svg
|
|
39
|
+
class="docs-page-nav-icon docs-page-nav-icon-next"
|
|
40
|
+
fill="none"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
>
|
|
45
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
46
|
+
</svg>
|
|
47
|
+
</a>
|
|
48
|
+
) : (
|
|
49
|
+
<div />
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</nav>
|
|
53
|
+
)}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavSection } from '../../navigation.ts'
|
|
3
|
+
import { ui } from '../../docs-config.ts'
|
|
4
|
+
import { formatChapterTitle } from '../../utils/docs.ts'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
sections: NavSection[]
|
|
8
|
+
locale?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { sections, locale } = Astro.props
|
|
12
|
+
const t = ui(locale)
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<aside class="docs-sidebar-left jaad-sidebar">
|
|
16
|
+
<nav aria-label={t('nav.documentation')}>
|
|
17
|
+
{sections.map((section) =>
|
|
18
|
+
section.type === 'page' ? (
|
|
19
|
+
<div class="docs-sidebar-page">
|
|
20
|
+
<a
|
|
21
|
+
href={section.item.href}
|
|
22
|
+
class="docs-sidebar-link"
|
|
23
|
+
aria-current={section.item.isActive ? 'page' : undefined}
|
|
24
|
+
>
|
|
25
|
+
{section.item.title}
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
) : (
|
|
29
|
+
<div class="docs-sidebar-chapter">
|
|
30
|
+
<h3 class="docs-sidebar-chapter-title">{formatChapterTitle(section.chapter)}</h3>
|
|
31
|
+
<ul class="docs-sidebar-chapter-list">
|
|
32
|
+
{section.items.map((item) => (
|
|
33
|
+
<li>
|
|
34
|
+
<a
|
|
35
|
+
href={item.href}
|
|
36
|
+
class="docs-sidebar-sublink"
|
|
37
|
+
aria-current={item.isActive ? 'page' : undefined}
|
|
38
|
+
>
|
|
39
|
+
{item.title}
|
|
40
|
+
</a>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
)}
|
|
47
|
+
</nav>
|
|
48
|
+
</aside>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
const SELECTOR = '.docs-sidebar-left'
|
|
52
|
+
let scrollTop = 0
|
|
53
|
+
|
|
54
|
+
const sidebar = () => document.querySelector<HTMLElement>(SELECTOR)
|
|
55
|
+
|
|
56
|
+
/** Scrolls the sidebar only, never the document, and only when needed. */
|
|
57
|
+
function revealCurrent() {
|
|
58
|
+
const el = sidebar()
|
|
59
|
+
const current = el?.querySelector('[aria-current="page"]')
|
|
60
|
+
if (!el || !current) return
|
|
61
|
+
|
|
62
|
+
const box = el.getBoundingClientRect()
|
|
63
|
+
const item = current.getBoundingClientRect()
|
|
64
|
+
if (item.top >= box.top && item.bottom <= box.bottom) return
|
|
65
|
+
|
|
66
|
+
el.scrollTop += item.top - box.top - (box.height - item.height) / 2
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.addEventListener('astro:before-swap', () => {
|
|
70
|
+
scrollTop = sidebar()?.scrollTop ?? 0
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
document.addEventListener('astro:after-swap', () => {
|
|
74
|
+
const el = sidebar()
|
|
75
|
+
if (!el) return
|
|
76
|
+
el.scrollTop = scrollTop
|
|
77
|
+
// A deep link, or a page whose entry sits outside the restored window.
|
|
78
|
+
revealCurrent()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
revealCurrent()
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { docsUrls, ui } from '../../docs-config.ts'
|
|
4
|
+
import { editUrl } from '../../infer.ts'
|
|
5
|
+
import { docsMarkdownHref } from '../../urls.ts'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Content collection entry id, e.g. "03-markdown/12-inner-html". */
|
|
9
|
+
id: string
|
|
10
|
+
/** URL-safe slug, e.g. "markdown/inner-html". */
|
|
11
|
+
cleanSlug: string
|
|
12
|
+
locale?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { id, cleanSlug, locale } = Astro.props
|
|
16
|
+
const t = ui(locale)
|
|
17
|
+
|
|
18
|
+
const mdHref = docsMarkdownHref(cleanSlug, docsUrls(locale))
|
|
19
|
+
const editHref = config.editBase ? editUrl(config.editBase, `${id}.md`) : null
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class="docs-page-actions">
|
|
23
|
+
<button type="button" data-copy-page data-md-href={mdHref} data-copied={t('page.copied')} class="docs-page-action">
|
|
24
|
+
<svg class="docs-page-action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
25
|
+
<path
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
stroke-width="2"
|
|
29
|
+
d="M16 1H4a2 2 0 00-2 2v14h2V3h12V1zm3 4H8a2 2 0 00-2 2v14a2 2 0 002 2h11a2 2 0 002-2V7a2 2 0 00-2-2z"
|
|
30
|
+
></path>
|
|
31
|
+
</svg>
|
|
32
|
+
<span data-copy-label>{t('page.copy')}</span>
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
{editHref && (
|
|
36
|
+
<a href={editHref} target="_blank" rel="noopener noreferrer" class="docs-page-action">
|
|
37
|
+
<svg class="docs-page-action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
38
|
+
<path
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
stroke-linejoin="round"
|
|
41
|
+
stroke-width="2"
|
|
42
|
+
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
<span>{t('page.edit')}</span>
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
function initCopyPageButtons(): void {
|
|
52
|
+
document.querySelectorAll<HTMLButtonElement>('[data-copy-page]').forEach((btn) => {
|
|
53
|
+
if (btn.dataset.copyInit) return
|
|
54
|
+
btn.dataset.copyInit = '1'
|
|
55
|
+
|
|
56
|
+
const label = btn.querySelector<HTMLElement>('[data-copy-label]')
|
|
57
|
+
|
|
58
|
+
btn.addEventListener('click', async () => {
|
|
59
|
+
const href = btn.dataset.mdHref
|
|
60
|
+
if (!href) return
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(href)
|
|
63
|
+
const text = await res.text()
|
|
64
|
+
await navigator.clipboard.writeText(text)
|
|
65
|
+
if (label) {
|
|
66
|
+
const original = label.textContent
|
|
67
|
+
label.textContent = btn.dataset.copied ?? 'Copied!'
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
label.textContent = original
|
|
70
|
+
}, 2000)
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Network or clipboard unavailable.
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
initCopyPageButtons()
|
|
80
|
+
document.addEventListener('astro:page-load', initCopyPageButtons)
|
|
81
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { docsUrls } from '../../docs-config.ts'
|
|
4
|
+
import { docsPageHref } from '../../urls.ts'
|
|
5
|
+
import { formatChapterTitle } from '../../utils/docs.ts'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string
|
|
9
|
+
description: string
|
|
10
|
+
chapter?: string
|
|
11
|
+
keywords?: string[]
|
|
12
|
+
author?: string
|
|
13
|
+
modified?: Date
|
|
14
|
+
/** BCP-47 tag of this page's locale. */
|
|
15
|
+
language?: string
|
|
16
|
+
locale?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { title, description, chapter, keywords = [], author, modified, language = config.lang, locale } = Astro.props
|
|
20
|
+
|
|
21
|
+
// Relative ids are worthless to a crawler, so without `site` emit nothing.
|
|
22
|
+
const site = Astro.site
|
|
23
|
+
const base = site ? docsPageHref('', docsUrls(locale)) : null
|
|
24
|
+
|
|
25
|
+
const crumbs = base
|
|
26
|
+
? [
|
|
27
|
+
{ name: config.title, url: new URL(base, Astro.url.origin).href },
|
|
28
|
+
...(chapter ? [{ name: formatChapterTitle(chapter)!, url: null }] : []),
|
|
29
|
+
{ name: title, url: Astro.url.href },
|
|
30
|
+
]
|
|
31
|
+
: []
|
|
32
|
+
|
|
33
|
+
const data = base && {
|
|
34
|
+
'@context': 'https://schema.org',
|
|
35
|
+
'@type': 'TechArticle',
|
|
36
|
+
headline: title,
|
|
37
|
+
description,
|
|
38
|
+
inLanguage: language,
|
|
39
|
+
...(keywords.length > 0 ? { keywords } : {}),
|
|
40
|
+
...(author ? { author: { '@type': 'Person', name: author } } : {}),
|
|
41
|
+
...(modified ? { dateModified: modified.toISOString() } : {}),
|
|
42
|
+
isPartOf: { '@type': 'WebSite', name: config.title },
|
|
43
|
+
breadcrumb: {
|
|
44
|
+
'@type': 'BreadcrumbList',
|
|
45
|
+
itemListElement: crumbs.map((crumb, i) => ({
|
|
46
|
+
'@type': 'ListItem',
|
|
47
|
+
position: i + 1,
|
|
48
|
+
name: crumb.name,
|
|
49
|
+
...(crumb.url ? { item: crumb.url } : {}),
|
|
50
|
+
})),
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
{data && <script type="application/ld+json" set:html={JSON.stringify(data)} is:inline />}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsHeadings } from '../../@types/docs.ts'
|
|
3
|
+
import { ui } from '../../docs-config.ts'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
headings: DocsHeadings[]
|
|
7
|
+
locale?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { headings, locale } = Astro.props
|
|
11
|
+
const t = ui(locale)
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{headings.length > 0 && (
|
|
15
|
+
<aside id="toc-sidebar" class="docs-sidebar-right jaad-sidebar">
|
|
16
|
+
<nav aria-label={t('toc.label')}>
|
|
17
|
+
<h2 class="docs-toc-title">{t('toc.title')}</h2>
|
|
18
|
+
<ul class="docs-toc-list">
|
|
19
|
+
{headings.map((heading) => (
|
|
20
|
+
<li>
|
|
21
|
+
<a
|
|
22
|
+
href={`#${heading.slug}`}
|
|
23
|
+
data-toc-link
|
|
24
|
+
class:list={['docs-toc-link', heading.depth === 3 && 'docs-toc-link-nested']}
|
|
25
|
+
>
|
|
26
|
+
{heading.text}
|
|
27
|
+
</a>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
</nav>
|
|
32
|
+
</aside>
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
function initToc(): void {
|
|
37
|
+
const sidebar = document.getElementById('toc-sidebar')
|
|
38
|
+
// astro:page-load also fires on the first load, which would observe twice.
|
|
39
|
+
if (!sidebar || sidebar.dataset.tocInit) return
|
|
40
|
+
sidebar.dataset.tocInit = '1'
|
|
41
|
+
|
|
42
|
+
const links = Array.from(sidebar.querySelectorAll<HTMLAnchorElement>('[data-toc-link]'))
|
|
43
|
+
const headingEls = links
|
|
44
|
+
.map((link) => document.getElementById(link.getAttribute('href')!.slice(1)))
|
|
45
|
+
.filter(Boolean) as HTMLElement[]
|
|
46
|
+
|
|
47
|
+
if (!headingEls.length) return
|
|
48
|
+
|
|
49
|
+
const setActive = (id: string): void => {
|
|
50
|
+
links.forEach((link) => {
|
|
51
|
+
const isActive = link.getAttribute('href') === `#${id}`
|
|
52
|
+
if (isActive) link.setAttribute('aria-current', 'location')
|
|
53
|
+
else link.removeAttribute('aria-current')
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const observer = new IntersectionObserver(
|
|
58
|
+
(entries) => {
|
|
59
|
+
entries.forEach((entry) => {
|
|
60
|
+
if (entry.isIntersecting) setActive(entry.target.id)
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
{ rootMargin: '-100px 0px -66%', threshold: 0 },
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
headingEls.forEach((el) => observer.observe(el))
|
|
67
|
+
|
|
68
|
+
document.addEventListener('astro:before-preparation', () => observer.disconnect(), { once: true })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
initToc()
|
|
72
|
+
document.addEventListener('astro:page-load', initToc)
|
|
73
|
+
</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
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { AstroUserConfig, FontProvider, Locales, SessionDriverConfig } from 'astro'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { z } from 'astro/zod'
|
|
7
|
+
import { inferRepo, inferDescription, editBaseFrom, type RepoInfo } from './infer.ts'
|
|
8
|
+
import { detectLocales, validateLocaleTree } from './locale-tree.ts'
|
|
9
|
+
import { describeLocale, isLocale, type Locale } from './locales.ts'
|
|
10
|
+
import { PRESETS, PRESET_NAMES, isPreset } from './themes/index.ts'
|
|
11
|
+
import { normaliseBasePath } from './urls.ts'
|
|
12
|
+
|
|
13
|
+
export const jaadConfigSchema = z.object({
|
|
14
|
+
title: z.string(),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
lang: z.string().default('en'),
|
|
17
|
+
logo: z
|
|
18
|
+
.string()
|
|
19
|
+
.refine((v) => v.startsWith('/') || /^https?:\/\//.test(v), {
|
|
20
|
+
message: 'logo must be a public url, such as "/logo.svg"',
|
|
21
|
+
})
|
|
22
|
+
.optional(),
|
|
23
|
+
|
|
24
|
+
docsDir: z.string().default('./docs'),
|
|
25
|
+
routeBase: z.string().default('/'),
|
|
26
|
+
|
|
27
|
+
social: z
|
|
28
|
+
.record(
|
|
29
|
+
z.string(),
|
|
30
|
+
z.union([
|
|
31
|
+
z.string(),
|
|
32
|
+
z.object({
|
|
33
|
+
href: z.string(),
|
|
34
|
+
label: z.string().optional(),
|
|
35
|
+
svg: z.string().optional(),
|
|
36
|
+
}),
|
|
37
|
+
]),
|
|
38
|
+
)
|
|
39
|
+
.default({}),
|
|
40
|
+
nav: z.array(z.object({ label: z.string(), href: z.string() })).default([]),
|
|
41
|
+
footer: z
|
|
42
|
+
.union([z.string(), z.literal(false)], {
|
|
43
|
+
error:
|
|
44
|
+
'footer is one line of text, or false to remove it. ' +
|
|
45
|
+
'The message/copyright pair is gone: write them as a single string, ' +
|
|
46
|
+
'e.g. "MIT Licensed · © 2026 Me · :credit".',
|
|
47
|
+
})
|
|
48
|
+
.optional(),
|
|
49
|
+
|
|
50
|
+
editLink: z.union([z.boolean(), z.string()]).default(true),
|
|
51
|
+
|
|
52
|
+
head: z
|
|
53
|
+
.array(
|
|
54
|
+
z.object({
|
|
55
|
+
tag: z.string(),
|
|
56
|
+
attrs: z.record(z.string(), z.union([z.string(), z.boolean()])).optional(),
|
|
57
|
+
content: z.string().optional(),
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
.default([]),
|
|
61
|
+
ogImage: z
|
|
62
|
+
.union([
|
|
63
|
+
z.string().refine((v) => v.startsWith('/') || /^https?:\/\//.test(v), {
|
|
64
|
+
message: 'ogImage must be a public url, such as "/og-image.png"',
|
|
65
|
+
}),
|
|
66
|
+
z.literal(false),
|
|
67
|
+
])
|
|
68
|
+
.optional(),
|
|
69
|
+
|
|
70
|
+
theme: z
|
|
71
|
+
.union([
|
|
72
|
+
z.string().refine(isPreset, {
|
|
73
|
+
message: `unknown theme; available: ${PRESET_NAMES.join(', ')}`,
|
|
74
|
+
}),
|
|
75
|
+
z.object({ light: z.string(), dark: z.string() }),
|
|
76
|
+
])
|
|
77
|
+
.default('default'),
|
|
78
|
+
|
|
79
|
+
appearance: z
|
|
80
|
+
.enum(['auto', 'light', 'dark'], {
|
|
81
|
+
error: 'unknown appearance; available: auto, light, dark',
|
|
82
|
+
})
|
|
83
|
+
.default('auto'),
|
|
84
|
+
|
|
85
|
+
/** Registers Tailwind (the site's own install) and exposes JAAD's tokens to it. */
|
|
86
|
+
tailwind: z.boolean().default(false),
|
|
87
|
+
|
|
88
|
+
/** Per-locale overrides for JAAD's bundled interface strings. */
|
|
89
|
+
ui: z.record(z.string(), z.record(z.string(), z.string())).default({}),
|
|
90
|
+
|
|
91
|
+
/** Only when detection guesses wrong: pin the set, or false to switch it off. */
|
|
92
|
+
locales: z
|
|
93
|
+
.union([z.array(z.string().refine(isLocale, { message: 'not a locale code' })), z.literal(false)])
|
|
94
|
+
.optional(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export type JaadConfig = z.output<typeof jaadConfigSchema>
|
|
98
|
+
|
|
99
|
+
/** Out of the schema, so `parse` strips them: the resolved config is
|
|
100
|
+
* serialised and an integration is not. */
|
|
101
|
+
type AstroPassthroughConfig = AstroUserConfig<Locales, string | SessionDriverConfig | undefined, FontProvider[]>
|
|
102
|
+
|
|
103
|
+
export type JaadUserConfig = z.input<typeof jaadConfigSchema> & {
|
|
104
|
+
site?: string
|
|
105
|
+
base?: string
|
|
106
|
+
astro?: AstroPassthroughConfig
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** What the components actually read: the user's options plus whatever the
|
|
110
|
+
* repository could tell us. Inferred fields are null when unavailable. */
|
|
111
|
+
export interface JaadResolvedConfig extends JaadConfig {
|
|
112
|
+
/** Where the docs are mounted, without a trailing slash. Empty at the root. */
|
|
113
|
+
docsBase: string
|
|
114
|
+
/** What jaamd gets for code blocks. */
|
|
115
|
+
shiki: string | { light: string; dark: string }
|
|
116
|
+
repoUrl: string | null
|
|
117
|
+
editBase: string | null
|
|
118
|
+
favicon: string | null
|
|
119
|
+
/** Explicit, inferred from public/, or false when no valid image exists. */
|
|
120
|
+
ogImage: string | false
|
|
121
|
+
/** Empty when the docs are not localised. */
|
|
122
|
+
docsLocales: Locale[]
|
|
123
|
+
/** `lang` when the docs are not localised. */
|
|
124
|
+
defaultLocale: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const FAVICONS = ['favicon.svg', 'favicon.ico', 'favicon.png']
|
|
128
|
+
const OG_IMAGES = ['og-image.png', 'og-image.jpg', 'og-image.jpeg', 'og-image.webp']
|
|
129
|
+
|
|
130
|
+
function findPublicAsset(cwd: string, candidates: string[]): string | null {
|
|
131
|
+
for (const name of candidates) {
|
|
132
|
+
if (existsSync(join(cwd, 'public', name))) return `/${name}`
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Empty at the root, otherwise one leading slash and no trailing one. */
|
|
138
|
+
function resolveEditBase(editLink: JaadConfig['editLink'], repo: RepoInfo | null, docsDir: string): string | null {
|
|
139
|
+
if (typeof editLink === 'string') return editLink
|
|
140
|
+
if (!editLink || !repo) return null
|
|
141
|
+
return editBaseFrom(repo, docsDir)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const OPTIONS = Object.keys(jaadConfigSchema.shape)
|
|
145
|
+
const FORWARDED = ['site', 'base', 'astro']
|
|
146
|
+
|
|
147
|
+
/** Unknown keys are rejected here: zod would strip a misspelt option in
|
|
148
|
+
* silence. Both messages are read in a terminal, not parsed. */
|
|
149
|
+
function parseConfig(options: JaadUserConfig): JaadConfig {
|
|
150
|
+
const known = new Set([...OPTIONS, ...FORWARDED])
|
|
151
|
+
const unknown = Object.keys(options).filter((key) => !known.has(key))
|
|
152
|
+
|
|
153
|
+
if (unknown.length > 0) {
|
|
154
|
+
const named = unknown.map((key) => {
|
|
155
|
+
const close = OPTIONS.find((o) => o.toLowerCase() === key.toLowerCase())
|
|
156
|
+
return close ? `${key} (did you mean ${close}?)` : key
|
|
157
|
+
})
|
|
158
|
+
throw new Error(
|
|
159
|
+
`jaad: unknown option${unknown.length > 1 ? 's' : ''} ${named.join(', ')}\n` +
|
|
160
|
+
` available: ${OPTIONS.join(', ')}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const parsed = jaadConfigSchema.safeParse(options)
|
|
165
|
+
if (parsed.success) return parsed.data
|
|
166
|
+
|
|
167
|
+
throw new Error(
|
|
168
|
+
'jaad: invalid configuration\n' +
|
|
169
|
+
parsed.error.issues.map((issue) => ` ${issue.path.join('.') || 'config'}: ${issue.message}`).join('\n'),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveConfig(options: JaadUserConfig, cwd: string = process.cwd()): JaadResolvedConfig {
|
|
174
|
+
const config = parseConfig(options)
|
|
175
|
+
const repo = inferRepo(cwd)
|
|
176
|
+
const locales = resolveLocales(config, cwd)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...config,
|
|
180
|
+
description: config.description ?? inferDescription(cwd) ?? undefined,
|
|
181
|
+
repoUrl: repo?.url ?? null,
|
|
182
|
+
favicon: findPublicAsset(cwd, FAVICONS),
|
|
183
|
+
ogImage: config.ogImage ?? findPublicAsset(cwd, OG_IMAGES) ?? false,
|
|
184
|
+
docsBase: normaliseBasePath(config.routeBase),
|
|
185
|
+
shiki: typeof config.theme === 'string' ? PRESETS[config.theme].shiki : config.theme,
|
|
186
|
+
editBase: resolveEditBase(config.editLink, repo, config.docsDir),
|
|
187
|
+
docsLocales: locales.map(describeLocale),
|
|
188
|
+
defaultLocale: locales.length > 0 ? config.lang.toLowerCase() : config.lang,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Directory names win; `locales` only overrides what detection found. */
|
|
193
|
+
function resolveLocales(config: JaadConfig, cwd: string): string[] {
|
|
194
|
+
if (config.locales === false) return []
|
|
195
|
+
|
|
196
|
+
const docsDir = join(cwd, config.docsDir)
|
|
197
|
+
const locales = config.locales ?? detectLocales(docsDir, config.lang)
|
|
198
|
+
// A pinned list skips detection, and with it the missing-directory guard;
|
|
199
|
+
// the integration warns about that case with better words than readdir does.
|
|
200
|
+
if (locales.length === 0 || !existsSync(docsDir)) return []
|
|
201
|
+
|
|
202
|
+
validateLocaleTree(docsDir, locales, config.lang)
|
|
203
|
+
return locales.map((locale) => locale.toLowerCase()).toSorted()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const requireFrom = createRequire(import.meta.url)
|
|
207
|
+
|
|
208
|
+
const styleInPackage = (name: string) => fileURLToPath(new URL(`./styles/${name}`, import.meta.url))
|
|
209
|
+
|
|
210
|
+
/** Absolute paths, kept off the resolved config, which gets serialised. */
|
|
211
|
+
export function resolveStylesheets(
|
|
212
|
+
config: JaadConfig,
|
|
213
|
+
cwd: string = process.cwd(),
|
|
214
|
+
): {
|
|
215
|
+
user: string | null
|
|
216
|
+
theme: string | null
|
|
217
|
+
bridge: string
|
|
218
|
+
tailwind: string | null
|
|
219
|
+
} {
|
|
220
|
+
const userCss = join(cwd, 'src', 'jaad.css')
|
|
221
|
+
const slug = typeof config.theme === 'string' ? PRESETS[config.theme].theme : null
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
user: existsSync(userCss) ? userCss : null,
|
|
225
|
+
// Resolved through JAAMD's export map rather than by path.
|
|
226
|
+
theme: slug ? requireFrom.resolve(`@jaamd/jaamd/themes/${slug}.css`) : null,
|
|
227
|
+
// A preset supplies the palette, so the colours travel the other way.
|
|
228
|
+
bridge: styleInPackage(slug ? 'jaamd-reverse.css' : 'jaamd-forward.css'),
|
|
229
|
+
tailwind: config.tailwind ? styleInPackage('tailwind.css') : null,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Identity helper, for a typed `jaad.config.ts`. */
|
|
234
|
+
export function defineJaadConfig(config: JaadUserConfig): JaadUserConfig {
|
|
235
|
+
return config
|
|
236
|
+
}
|