@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,23 @@
|
|
|
1
|
+
import type { DocsNavigation } from './navigation.ts'
|
|
2
|
+
import type { DocsHeading, DocsPageData } from './types.ts'
|
|
3
|
+
|
|
4
|
+
/** Data available to an advanced documentation frame. */
|
|
5
|
+
export interface DocsFrameProps {
|
|
6
|
+
page: {
|
|
7
|
+
/** Content collection id, without the Markdown extension. */
|
|
8
|
+
id: string
|
|
9
|
+
/** File-derived slug. The opening page keeps its named slug here. */
|
|
10
|
+
slug: string
|
|
11
|
+
title: string
|
|
12
|
+
/** Set when the docs are split into sections. */
|
|
13
|
+
section?: string
|
|
14
|
+
chapter?: string
|
|
15
|
+
/** Raw frontmatter. */
|
|
16
|
+
data: DocsPageData
|
|
17
|
+
}
|
|
18
|
+
headings: DocsHeading[]
|
|
19
|
+
navigation: DocsNavigation
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type { DocsHeading, DocsNavItem, DocsPageData } from './types.ts'
|
|
23
|
+
export type { DocsNavEntry, DocsNavGroup, DocsNavigation } from './navigation.ts'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { LocaleAlternate } from '../site/locales.ts'
|
|
2
|
+
import type { DocsSource, DocsTree } from './model/tree.ts'
|
|
3
|
+
import type { DocsEntry } from './types.ts'
|
|
4
|
+
import { getCollection } from 'astro:content'
|
|
5
|
+
import config from 'virtual:jaad/config'
|
|
6
|
+
import { DEFAULT_LOCALE, LOCALES, prefixedCodes } from '../site/runtime.ts'
|
|
7
|
+
import { collectionName, splitDocLocale } from './model/ids.ts'
|
|
8
|
+
import { buildDocsTree } from './model/tree.ts'
|
|
9
|
+
import { validateDocsStructure } from './model/validate.ts'
|
|
10
|
+
import { SECTIONS, docsUrls } from './runtime.ts'
|
|
11
|
+
import { docsPageHref } from './urls.ts'
|
|
12
|
+
|
|
13
|
+
/** A collection entry, placed in its locale and section. */
|
|
14
|
+
export type DocsSourceEntry = DocsEntry & DocsSource
|
|
15
|
+
|
|
16
|
+
const cached = new Map<string, DocsTree<DocsSourceEntry>>()
|
|
17
|
+
|
|
18
|
+
const CODES = LOCALES.map((locale) => locale.code)
|
|
19
|
+
|
|
20
|
+
const KEYS = SECTIONS.map((section) => section.key)
|
|
21
|
+
|
|
22
|
+
/** Attaches the locale, the section and the path, leaving `id` the collection id. */
|
|
23
|
+
function place(entry: DocsEntry, section?: string): DocsSourceEntry {
|
|
24
|
+
const { locale, id } = splitDocLocale(entry.id, CODES)
|
|
25
|
+
return {
|
|
26
|
+
...entry,
|
|
27
|
+
locale: locale ?? undefined,
|
|
28
|
+
section,
|
|
29
|
+
path: section ? id.slice(section.length + 1) : id,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The pages of one locale, or every page when the docs are not localised.
|
|
34
|
+
* Cached in production only: in dev the module would go stale on edit. */
|
|
35
|
+
export async function getDocsTree(locale: string = DEFAULT_LOCALE): Promise<DocsTree<DocsSourceEntry>> {
|
|
36
|
+
const key = CODES.length > 0 ? locale : ''
|
|
37
|
+
const hit = cached.get(key)
|
|
38
|
+
if (import.meta.env.PROD && hit) return hit
|
|
39
|
+
|
|
40
|
+
// `docsDir` when in use, then one collection per section.
|
|
41
|
+
const sections = [...(config.docs.dirInUse ? [undefined] : []), ...KEYS]
|
|
42
|
+
const collections = await Promise.all(
|
|
43
|
+
sections.map(async (section) => {
|
|
44
|
+
const entries = await getCollection(collectionName(section) as 'docsPages')
|
|
45
|
+
return (entries as unknown as DocsEntry[]).map((entry) => place(entry, section))
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
const sources = collections.flat().filter((source) => key === '' || source.locale === key)
|
|
49
|
+
|
|
50
|
+
// Validated before drafts are dropped, so a draft's slug conflict is an error.
|
|
51
|
+
validateDocsStructure(sources, key || undefined)
|
|
52
|
+
const tree = buildDocsTree(import.meta.env.PROD ? sources.filter((source) => !source.data.draft) : sources, {
|
|
53
|
+
sections: KEYS,
|
|
54
|
+
urls: docsUrls(locale),
|
|
55
|
+
})
|
|
56
|
+
if (import.meta.env.PROD) cached.set(key, tree)
|
|
57
|
+
return tree
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SectionLink {
|
|
61
|
+
key: string
|
|
62
|
+
label: string
|
|
63
|
+
href: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The sections with something to serve in a locale, each at the url of its
|
|
67
|
+
* first page. */
|
|
68
|
+
export async function getSectionLinks(locale: string = DEFAULT_LOCALE): Promise<SectionLink[]> {
|
|
69
|
+
const { pages } = await getDocsTree(locale)
|
|
70
|
+
|
|
71
|
+
return SECTIONS.flatMap(({ key, label }) => {
|
|
72
|
+
const first = pages.find((page) => page.section === key)
|
|
73
|
+
return first ? [{ key, label, href: first.href }] : []
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Where a page lives in every locale, paired by `translationKey` or by slug.
|
|
78
|
+
* An untranslated page falls back to that locale's opening page. Empty asks
|
|
79
|
+
* for the opening pages themselves. */
|
|
80
|
+
export async function getLocaleAlternates(key: string): Promise<LocaleAlternate[]> {
|
|
81
|
+
const alternates: LocaleAlternate[] = []
|
|
82
|
+
|
|
83
|
+
for (const locale of LOCALES) {
|
|
84
|
+
const tree = await getDocsTree(locale.code)
|
|
85
|
+
// Every page drafted leaves a locale with nothing to link to.
|
|
86
|
+
if (tree.pages.length === 0) continue
|
|
87
|
+
|
|
88
|
+
const match = key ? tree.byKey.get(key) : tree.pages[0]
|
|
89
|
+
alternates.push({
|
|
90
|
+
locale,
|
|
91
|
+
translated: Boolean(match),
|
|
92
|
+
href: match ? match.href : docsPageHref('', docsUrls(locale.code)),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return alternates
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The prefixed locales that have something to serve. */
|
|
100
|
+
export async function getServedLocales(): Promise<string[]> {
|
|
101
|
+
const served: string[] = []
|
|
102
|
+
|
|
103
|
+
for (const code of prefixedCodes()) {
|
|
104
|
+
const { pages } = await getDocsTree(code)
|
|
105
|
+
if (pages.length > 0) served.push(code)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return served
|
|
109
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ui } from '../../site/runtime.ts'
|
|
3
|
+
import { getSectionLinks } from '../collection.ts'
|
|
4
|
+
import { formatChapterTitle } from '../model/ids.ts'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** The key of the section the page belongs to. */
|
|
8
|
+
section?: string
|
|
9
|
+
chapter?: string
|
|
10
|
+
locale?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { section, chapter, locale } = Astro.props
|
|
14
|
+
const t = ui(locale)
|
|
15
|
+
const chapterTitle = formatChapterTitle(chapter)
|
|
16
|
+
const sectionLink = section ? (await getSectionLinks(locale)).find((link) => link.key === section) : undefined
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{(sectionLink || chapterTitle) && (
|
|
20
|
+
<nav aria-label={t('breadcrumb.label')} class="docs-breadcrumbs">
|
|
21
|
+
{sectionLink && (
|
|
22
|
+
<a href={sectionLink.href} class="docs-breadcrumbs-link">
|
|
23
|
+
{sectionLink.label}
|
|
24
|
+
</a>
|
|
25
|
+
)}
|
|
26
|
+
{sectionLink && chapterTitle && <span aria-hidden="true">·</span>}
|
|
27
|
+
{chapterTitle && <span>{chapterTitle}</span>}
|
|
28
|
+
</nav>
|
|
29
|
+
)}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsNavGroup } from '../navigation.ts'
|
|
3
|
+
import type { DocsHeading, DocsNavItem } from '../types.ts'
|
|
4
|
+
import { ui } from '../../site/runtime.ts'
|
|
5
|
+
import { formatChapterTitle } from '../model/ids.ts'
|
|
6
|
+
import DocsNavTree from './DocsNavTree.astro'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
locale?: string
|
|
10
|
+
groups: DocsNavGroup[]
|
|
11
|
+
headings: DocsHeading[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { groups, headings, locale } = Astro.props
|
|
15
|
+
const t = ui(locale)
|
|
16
|
+
|
|
17
|
+
/** Section, chapter and page of the current page, as the menu's label. */
|
|
18
|
+
function trail(): string[] {
|
|
19
|
+
for (const group of groups) {
|
|
20
|
+
const entries = group.type === 'section' ? group.entries : [group]
|
|
21
|
+
const section = group.type === 'section' ? [group.label] : []
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const items: DocsNavItem[] = entry.type === 'page' ? [entry.item] : entry.items
|
|
24
|
+
const current = items.find((item) => item.isActive)
|
|
25
|
+
if (!current) continue
|
|
26
|
+
const chapter = entry.type === 'chapter' ? [formatChapterTitle(entry.chapter) ?? entry.chapter] : []
|
|
27
|
+
return [...section, ...chapter, current.title]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const path = trail()
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<div class="docs-mobile-nav">
|
|
37
|
+
<details class="docs-mobile-menu">
|
|
38
|
+
<summary class="docs-mobile-summary">
|
|
39
|
+
<svg class="docs-mobile-icon" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
|
|
40
|
+
<path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z" />
|
|
41
|
+
</svg>
|
|
42
|
+
<span class="jaad-sr-only">{t('nav.documentation')}:</span>
|
|
43
|
+
{path.map((part, index) => (
|
|
44
|
+
<>
|
|
45
|
+
{index > 0 && <span aria-hidden="true">/</span>}
|
|
46
|
+
<span class:list={[index === path.length - 1 && 'docs-mobile-current']}>{part}</span>
|
|
47
|
+
</>
|
|
48
|
+
))}
|
|
49
|
+
</summary>
|
|
50
|
+
<nav aria-label={t('nav.documentation')} class="docs-mobile-tree">
|
|
51
|
+
<DocsNavTree {groups} />
|
|
52
|
+
</nav>
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
{headings.length > 0 && (
|
|
56
|
+
<details class="docs-mobile-menu">
|
|
57
|
+
<summary class="docs-mobile-summary">{t('toc.title')}</summary>
|
|
58
|
+
<nav aria-label={t('toc.label')} class="docs-mobile-tree">
|
|
59
|
+
<ul class="docs-toc-list">
|
|
60
|
+
{headings.map((heading) => (
|
|
61
|
+
<li>
|
|
62
|
+
<a
|
|
63
|
+
href={`#${heading.slug}`}
|
|
64
|
+
class:list={['docs-toc-link', heading.depth === 3 && 'docs-toc-link-nested']}
|
|
65
|
+
>
|
|
66
|
+
{heading.text}
|
|
67
|
+
</a>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
</nav>
|
|
72
|
+
</details>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
// A chosen entry closes its menu, so the page it opens is not covered.
|
|
78
|
+
function closeOnChoice(): void {
|
|
79
|
+
for (const menu of document.querySelectorAll<HTMLDetailsElement>('.docs-mobile-menu')) {
|
|
80
|
+
if (menu.dataset.navInit) continue
|
|
81
|
+
menu.dataset.navInit = '1'
|
|
82
|
+
menu.addEventListener('click', (event) => {
|
|
83
|
+
if ((event.target as Element).closest('a')) menu.open = false
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
closeOnChoice()
|
|
89
|
+
document.addEventListener('astro:page-load', closeOnChoice)
|
|
90
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsNavEntry, DocsNavGroup } from '../navigation.ts'
|
|
3
|
+
import { formatChapterTitle } from '../model/ids.ts'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
groups: DocsNavGroup[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { groups } = Astro.props
|
|
10
|
+
|
|
11
|
+
/** The pages outside every section, then each section with all its pages. */
|
|
12
|
+
const loose = groups.filter((group): group is DocsNavEntry => group.type !== 'section')
|
|
13
|
+
const blocks = [
|
|
14
|
+
...(loose.length > 0 ? [{ entries: loose, label: null }] : []),
|
|
15
|
+
...groups.flatMap((group) => (group.type === 'section' ? [{ entries: group.entries, label: group.label }] : [])),
|
|
16
|
+
]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{blocks.map(({ entries, label }) => (
|
|
20
|
+
<div class:list={[label && 'docs-sidebar-section']}>
|
|
21
|
+
{label && <p class="docs-sidebar-section-title">{label}</p>}
|
|
22
|
+
{entries.length > 0 && (
|
|
23
|
+
<ul class="docs-sidebar-list">
|
|
24
|
+
{entries.map((entry) =>
|
|
25
|
+
entry.type === 'page' ? (
|
|
26
|
+
<li>
|
|
27
|
+
<a
|
|
28
|
+
href={entry.item.href}
|
|
29
|
+
class="docs-sidebar-link"
|
|
30
|
+
aria-current={entry.item.isActive ? 'page' : undefined}
|
|
31
|
+
>
|
|
32
|
+
{entry.item.title}
|
|
33
|
+
</a>
|
|
34
|
+
</li>
|
|
35
|
+
) : (
|
|
36
|
+
<li class="docs-sidebar-chapter">
|
|
37
|
+
<p class="docs-sidebar-chapter-title">{formatChapterTitle(entry.chapter)}</p>
|
|
38
|
+
<ul class="docs-sidebar-list">
|
|
39
|
+
{entry.items.map((item) => (
|
|
40
|
+
<li>
|
|
41
|
+
<a
|
|
42
|
+
href={item.href}
|
|
43
|
+
class="docs-sidebar-link"
|
|
44
|
+
aria-current={item.isActive ? 'page' : undefined}
|
|
45
|
+
>
|
|
46
|
+
{item.title}
|
|
47
|
+
</a>
|
|
48
|
+
</li>
|
|
49
|
+
))}
|
|
50
|
+
</ul>
|
|
51
|
+
</li>
|
|
52
|
+
),
|
|
53
|
+
)}
|
|
54
|
+
</ul>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsSourceEntry } from '../collection.ts'
|
|
3
|
+
import type { DocsPage, DocsTree } from '../model/tree.ts'
|
|
4
|
+
import MarkdownContent from '@jaamd/astro/components'
|
|
5
|
+
import config from 'virtual:jaad/config'
|
|
6
|
+
import { DEFAULT_LOCALE, LOCALES, ui } from '../../site/runtime.ts'
|
|
7
|
+
import DocsLayout from '../layouts/Docs.astro'
|
|
8
|
+
import { pageHeadings } from '../model/rendered.ts'
|
|
9
|
+
import { buildDocsNavigation } from '../navigation.ts'
|
|
10
|
+
import { SECTIONS } from '../runtime.ts'
|
|
11
|
+
import Breadcrumbs from './Breadcrumbs.astro'
|
|
12
|
+
import DocsMobileNav from './DocsMobileNav.astro'
|
|
13
|
+
import DocsPageNav from './DocsPageNav.astro'
|
|
14
|
+
import PageActions from './PageActions.astro'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
page: DocsPage<DocsSourceEntry>
|
|
18
|
+
tree: DocsTree<DocsSourceEntry>
|
|
19
|
+
/** Left out on the opening page, whose title is the site name alone. */
|
|
20
|
+
headTitle?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { page, tree, headTitle } = Astro.props
|
|
24
|
+
|
|
25
|
+
const section = SECTIONS.find((known) => known.key === page.section)
|
|
26
|
+
// With sections, the title names the section after the page.
|
|
27
|
+
const title = headTitle && section ? `${headTitle} · ${section.label}` : headTitle
|
|
28
|
+
// A section's files sit in its own directory.
|
|
29
|
+
const editBase = section ? section.editBase : config.docs.editBase
|
|
30
|
+
const editPath = [page.locale, page.path].filter(Boolean).join('/')
|
|
31
|
+
const headings = pageHeadings(page.source.rendered)
|
|
32
|
+
const t = ui(page.locale)
|
|
33
|
+
const language = LOCALES.find((known) => known.code === (page.locale ?? DEFAULT_LOCALE))?.tag ?? config.lang
|
|
34
|
+
const updated = page.data.lastUpdated
|
|
35
|
+
const navigation = buildDocsNavigation(tree, page.id, SECTIONS)
|
|
36
|
+
const contentAttrs = { id: 'content' }
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<DocsLayout {title} {page} {headings} {navigation}>
|
|
40
|
+
<DocsMobileNav slot="mobile-navigation" groups={navigation.groups} headings={headings} locale={page.locale} />
|
|
41
|
+
|
|
42
|
+
<Breadcrumbs slot="breadcrumbs" section={page.section} chapter={page.chapter} locale={page.locale} />
|
|
43
|
+
<PageActions slot="page-actions" {editBase} {editPath} cleanSlug={page.slug} locale={page.locale} />
|
|
44
|
+
|
|
45
|
+
<Fragment slot="content">
|
|
46
|
+
<MarkdownContent {...contentAttrs} class="docs-content">
|
|
47
|
+
<slot />
|
|
48
|
+
</MarkdownContent>
|
|
49
|
+
|
|
50
|
+
{updated && (
|
|
51
|
+
<p class="docs-updated">
|
|
52
|
+
{t('page.lastUpdated')}{' '}
|
|
53
|
+
<time datetime={updated.toISOString()}>
|
|
54
|
+
{new Intl.DateTimeFormat(language, { dateStyle: 'long' }).format(updated)}
|
|
55
|
+
</time>
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
</Fragment>
|
|
59
|
+
|
|
60
|
+
<DocsPageNav
|
|
61
|
+
slot="page-navigation"
|
|
62
|
+
previousPage={navigation.previousPage}
|
|
63
|
+
nextPage={navigation.nextPage}
|
|
64
|
+
locale={page.locale}
|
|
65
|
+
/>
|
|
66
|
+
</DocsLayout>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsNavItem } from '../types.ts'
|
|
3
|
+
import { ui } from '../../site/runtime.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"
|
|
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"
|
|
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,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocsNavGroup } from '../navigation.ts'
|
|
3
|
+
import { ui } from '../../site/runtime.ts'
|
|
4
|
+
import DocsNavTree from './DocsNavTree.astro'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
groups: DocsNavGroup[]
|
|
8
|
+
locale?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { groups, 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
|
+
<DocsNavTree {groups} />
|
|
18
|
+
</nav>
|
|
19
|
+
</aside>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
const SELECTOR = '.docs-sidebar-left'
|
|
23
|
+
let scrollTop = 0
|
|
24
|
+
|
|
25
|
+
const sidebar = () => document.querySelector<HTMLElement>(SELECTOR)
|
|
26
|
+
|
|
27
|
+
/** Scrolls the sidebar only, never the document, and only when needed. */
|
|
28
|
+
function revealCurrent() {
|
|
29
|
+
const el = sidebar()
|
|
30
|
+
const current = el?.querySelector('[aria-current="page"]')
|
|
31
|
+
if (!el || !current) return
|
|
32
|
+
|
|
33
|
+
const box = el.getBoundingClientRect()
|
|
34
|
+
const item = current.getBoundingClientRect()
|
|
35
|
+
if (item.top >= box.top && item.bottom <= box.bottom) return
|
|
36
|
+
|
|
37
|
+
el.scrollTop += item.top - box.top - (box.height - item.height) / 2
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.addEventListener('astro:before-swap', () => {
|
|
41
|
+
scrollTop = sidebar()?.scrollTop ?? 0
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
document.addEventListener('astro:after-swap', () => {
|
|
45
|
+
const el = sidebar()
|
|
46
|
+
if (!el) return
|
|
47
|
+
el.scrollTop = scrollTop
|
|
48
|
+
// A deep link, or a page whose entry sits outside the restored window.
|
|
49
|
+
revealCurrent()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
revealCurrent()
|
|
53
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { editUrl } from '../../site/infer.ts'
|
|
3
|
+
import { ui } from '../../site/runtime.ts'
|
|
4
|
+
import { docsUrls } from '../runtime.ts'
|
|
5
|
+
import { docsMarkdownHref } from '../urls.ts'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Where the page's directory is edited, or null to leave the link out. */
|
|
9
|
+
editBase: string | null
|
|
10
|
+
/** The file below that directory, without its extension. */
|
|
11
|
+
editPath: string
|
|
12
|
+
/** URL-safe slug, e.g. "markdown/inner-html". */
|
|
13
|
+
cleanSlug: string
|
|
14
|
+
locale?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { editBase, editPath, cleanSlug, locale } = Astro.props
|
|
18
|
+
const t = ui(locale)
|
|
19
|
+
|
|
20
|
+
const mdHref = docsMarkdownHref(cleanSlug, docsUrls(locale))
|
|
21
|
+
const editHref = editBase ? editUrl(editBase, `${editPath}.md`) : null
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div class="docs-page-actions">
|
|
25
|
+
<button type="button" data-copy-page data-md-href={mdHref} data-copied={t('page.copied')} class="docs-page-action">
|
|
26
|
+
<svg class="docs-page-action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
27
|
+
<path
|
|
28
|
+
stroke-linecap="round"
|
|
29
|
+
stroke-linejoin="round"
|
|
30
|
+
stroke-width="2"
|
|
31
|
+
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"
|
|
32
|
+
></path>
|
|
33
|
+
</svg>
|
|
34
|
+
<span data-copy-label>{t('page.copy')}</span>
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
{editHref && (
|
|
38
|
+
<a href={editHref} target="_blank" rel="noopener noreferrer" class="docs-page-action">
|
|
39
|
+
<svg class="docs-page-action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
40
|
+
<path
|
|
41
|
+
stroke-linecap="round"
|
|
42
|
+
stroke-linejoin="round"
|
|
43
|
+
stroke-width="2"
|
|
44
|
+
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"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
<span>{t('page.edit')}</span>
|
|
48
|
+
</a>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
function initCopyPageButtons(): void {
|
|
54
|
+
document.querySelectorAll<HTMLButtonElement>('[data-copy-page]').forEach((btn) => {
|
|
55
|
+
if (btn.dataset.copyInit) return
|
|
56
|
+
btn.dataset.copyInit = '1'
|
|
57
|
+
|
|
58
|
+
const label = btn.querySelector<HTMLElement>('[data-copy-label]')
|
|
59
|
+
|
|
60
|
+
btn.addEventListener('click', async () => {
|
|
61
|
+
const href = btn.dataset.mdHref
|
|
62
|
+
if (!href) return
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(href)
|
|
65
|
+
const text = await res.text()
|
|
66
|
+
await navigator.clipboard.writeText(text)
|
|
67
|
+
if (label) {
|
|
68
|
+
const original = label.textContent
|
|
69
|
+
label.textContent = btn.dataset.copied ?? 'Copied!'
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
label.textContent = original
|
|
72
|
+
}, 2000)
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Network or clipboard unavailable.
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
initCopyPageButtons()
|
|
82
|
+
document.addEventListener('astro:page-load', initCopyPageButtons)
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { formatChapterTitle } from '../model/ids.ts'
|
|
4
|
+
import { docsUrls } from '../runtime.ts'
|
|
5
|
+
import { docsPageHref } from '../urls.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 />}
|