@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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/jaad.css +1331 -0
  4. package/docs/00-overview.md +25 -0
  5. package/docs/01-getting-started/01-installation.md +216 -0
  6. package/docs/01-getting-started/02-pages.md +109 -0
  7. package/docs/01-getting-started/03-deployment.md +100 -0
  8. package/docs/02-configurations/01-reference.md +215 -0
  9. package/docs/02-configurations/02-frontmatter.md +105 -0
  10. package/docs/02-configurations/03-i18n.md +210 -0
  11. package/docs/02-configurations/04-search.md +58 -0
  12. package/docs/02-configurations/05-themes.md +104 -0
  13. package/docs/02-configurations/06-styles.md +156 -0
  14. package/docs/02-configurations/07-custom-pages.md +119 -0
  15. package/docs/02-configurations/08-advanced-layout.md +139 -0
  16. package/docs/03-upgrading.md +208 -0
  17. package/index.ts +31 -0
  18. package/package.json +71 -3
  19. package/site.ts +36 -0
  20. package/src/config.ts +85 -0
  21. package/src/docs/advanced.ts +23 -0
  22. package/src/docs/collection.ts +109 -0
  23. package/src/docs/components/Breadcrumbs.astro +29 -0
  24. package/src/docs/components/DocsMobileNav.astro +90 -0
  25. package/src/docs/components/DocsNavTree.astro +57 -0
  26. package/src/docs/components/DocsPage.astro +66 -0
  27. package/src/docs/components/DocsPageNav.astro +53 -0
  28. package/src/docs/components/DocsSidebarLeft.astro +53 -0
  29. package/src/docs/components/PageActions.astro +83 -0
  30. package/src/docs/components/StructuredData.astro +55 -0
  31. package/src/docs/components/TableOfContents.astro +71 -0
  32. package/src/docs/config.ts +167 -0
  33. package/src/docs/content.ts +36 -0
  34. package/src/docs/endpoints.ts +89 -0
  35. package/src/docs/integration.ts +249 -0
  36. package/src/docs/layouts/DefaultDocsFrame.astro +33 -0
  37. package/src/docs/layouts/Docs.astro +169 -0
  38. package/src/docs/layouts/Page.astro +18 -0
  39. package/src/docs/link-plugins.ts +52 -0
  40. package/src/docs/links.ts +42 -0
  41. package/src/docs/locale-tree.ts +51 -0
  42. package/src/docs/model/ids.ts +87 -0
  43. package/src/docs/model/rendered.ts +185 -0
  44. package/src/docs/model/tree.ts +124 -0
  45. package/src/docs/model/validate.ts +75 -0
  46. package/src/docs/navigation.ts +75 -0
  47. package/src/docs/routes/404.astro +16 -0
  48. package/src/docs/routes/docs-index.astro +19 -0
  49. package/src/docs/routes/docs-slug.astro +54 -0
  50. package/src/docs/routes/docs-slug.md.ts +35 -0
  51. package/src/docs/routes/llms-locale.txt.ts +8 -0
  52. package/src/docs/routes/llms.txt.ts +4 -0
  53. package/src/docs/routes/search-index-locale.json.ts +8 -0
  54. package/src/docs/routes/search-index.json.ts +4 -0
  55. package/src/docs/runtime.ts +17 -0
  56. package/src/docs/search-index.ts +39 -0
  57. package/src/docs/styles/components/breadcrumbs.css +26 -0
  58. package/src/docs/styles/components/docs-mobile-nav.css +47 -0
  59. package/src/docs/styles/components/docs-page-nav.css +41 -0
  60. package/src/docs/styles/components/docs-page.css +9 -0
  61. package/src/docs/styles/components/docs-sidebar.css +59 -0
  62. package/src/docs/styles/components/page-actions.css +51 -0
  63. package/src/docs/styles/components/toc.css +38 -0
  64. package/src/docs/styles/components.css +7 -0
  65. package/src/docs/styles/docs.css +81 -0
  66. package/src/docs/styles/jaamd.css +11 -0
  67. package/src/docs/types.ts +39 -0
  68. package/src/docs/urls.ts +35 -0
  69. package/src/env.d.ts +13 -0
  70. package/src/integration.ts +36 -0
  71. package/src/site/components/Footer.astro +18 -0
  72. package/src/site/components/Head.astro +119 -0
  73. package/src/site/components/Header.astro +50 -0
  74. package/src/site/components/LocaleSwitcher.astro +111 -0
  75. package/src/site/components/Nav.astro +22 -0
  76. package/src/site/components/NavMobile.astro +105 -0
  77. package/src/site/components/SearchPalette.astro +409 -0
  78. package/src/site/components/SocialLinks.astro +27 -0
  79. package/src/site/components/ThemeSwitcher.astro +55 -0
  80. package/src/site/components/locale-switcher.ts +6 -0
  81. package/src/site/config.ts +149 -0
  82. package/src/site/fonts/OFL.txt +94 -0
  83. package/src/site/fonts/inter.woff2 +0 -0
  84. package/src/site/fonts/merriweather-italic.woff2 +0 -0
  85. package/src/site/fonts/merriweather.woff2 +0 -0
  86. package/src/site/fonts.ts +29 -0
  87. package/src/site/forges.ts +60 -0
  88. package/src/site/helpers.ts +4 -0
  89. package/src/site/infer.ts +75 -0
  90. package/src/site/integration.ts +59 -0
  91. package/src/site/layouts/Shell.astro +41 -0
  92. package/src/site/locales.ts +199 -0
  93. package/src/site/runtime.ts +19 -0
  94. package/src/site/search.ts +227 -0
  95. package/src/site/social.ts +44 -0
  96. package/src/site/styles/components/footer.css +8 -0
  97. package/src/site/styles/components/header.css +31 -0
  98. package/src/site/styles/components/locale-switcher.css +20 -0
  99. package/src/site/styles/components/nav-mobile.css +15 -0
  100. package/src/site/styles/components/nav.css +11 -0
  101. package/src/site/styles/components/not-found.css +11 -0
  102. package/src/site/styles/components/search.css +118 -0
  103. package/src/site/styles/components/shell.css +77 -0
  104. package/src/site/styles/components/social-links.css +8 -0
  105. package/src/site/styles/components/theme-switcher.css +14 -0
  106. package/src/site/styles/components.css +10 -0
  107. package/src/site/styles/global.css +140 -0
  108. package/src/site/styles/jaamd-forward.css +16 -0
  109. package/src/site/styles/jaamd-reverse.css +18 -0
  110. package/src/site/styles/tailwind.css +30 -0
  111. package/src/site/themes.ts +26 -0
  112. package/src/site/ui/en.ts +27 -0
  113. package/src/site/ui/it.ts +28 -0
  114. package/src/site/ui.ts +49 -0
  115. package/src/site/urls.ts +14 -0
  116. package/src/site/virtual.ts +42 -0
  117. 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 />}