@jaamd/jaad 0.11.0 → 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 (138) hide show
  1. package/README.md +8 -2
  2. package/dist/jaad.css +232 -347
  3. package/docs/00-overview.md +25 -0
  4. package/docs/01-getting-started/01-installation.md +216 -0
  5. package/docs/01-getting-started/02-pages.md +109 -0
  6. package/docs/01-getting-started/03-deployment.md +100 -0
  7. package/docs/02-configurations/01-reference.md +215 -0
  8. package/docs/02-configurations/02-frontmatter.md +105 -0
  9. package/docs/02-configurations/03-i18n.md +210 -0
  10. package/docs/02-configurations/04-search.md +58 -0
  11. package/docs/02-configurations/05-themes.md +104 -0
  12. package/docs/02-configurations/06-styles.md +156 -0
  13. package/docs/02-configurations/07-custom-pages.md +119 -0
  14. package/docs/02-configurations/08-advanced-layout.md +139 -0
  15. package/docs/03-upgrading.md +208 -0
  16. package/index.ts +8 -22
  17. package/package.json +10 -9
  18. package/site.ts +18 -1
  19. package/src/config.ts +25 -176
  20. package/src/{advanced.ts → docs/advanced.ts} +6 -4
  21. package/src/docs/collection.ts +109 -0
  22. package/src/docs/components/Breadcrumbs.astro +29 -0
  23. package/src/docs/components/DocsMobileNav.astro +90 -0
  24. package/src/docs/components/DocsNavTree.astro +57 -0
  25. package/src/docs/components/DocsPage.astro +66 -0
  26. package/src/{components/docs → docs/components}/DocsPageNav.astro +4 -4
  27. package/src/docs/components/DocsSidebarLeft.astro +53 -0
  28. package/src/{components/docs → docs/components}/PageActions.astro +10 -8
  29. package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
  30. package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
  31. package/src/docs/config.ts +167 -0
  32. package/src/docs/content.ts +36 -0
  33. package/src/docs/endpoints.ts +89 -0
  34. package/src/docs/integration.ts +249 -0
  35. package/src/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
  36. package/src/{layouts → docs/layouts}/Docs.astro +44 -43
  37. package/src/docs/layouts/Page.astro +18 -0
  38. package/src/docs/link-plugins.ts +52 -0
  39. package/src/docs/links.ts +42 -0
  40. package/src/{locale-tree.ts → docs/locale-tree.ts} +1 -1
  41. package/src/docs/model/ids.ts +87 -0
  42. package/src/docs/model/rendered.ts +185 -0
  43. package/src/docs/model/tree.ts +124 -0
  44. package/src/docs/model/validate.ts +75 -0
  45. package/src/docs/navigation.ts +75 -0
  46. package/src/{routes → docs/routes}/404.astro +1 -1
  47. package/src/docs/routes/docs-index.astro +19 -0
  48. package/src/docs/routes/docs-slug.astro +54 -0
  49. package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
  50. package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
  51. package/src/{routes → docs/routes}/llms.txt.ts +1 -1
  52. package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
  53. package/src/{routes → docs/routes}/search-index.json.ts +1 -1
  54. package/src/docs/runtime.ts +17 -0
  55. package/src/docs/search-index.ts +39 -0
  56. package/src/docs/styles/components/breadcrumbs.css +26 -0
  57. package/src/docs/styles/components/docs-mobile-nav.css +47 -0
  58. package/src/docs/styles/components/docs-page-nav.css +41 -0
  59. package/src/docs/styles/components/docs-sidebar.css +59 -0
  60. package/src/docs/styles/components/page-actions.css +51 -0
  61. package/src/docs/styles/components/toc.css +38 -0
  62. package/src/docs/styles/components.css +7 -0
  63. package/src/docs/styles/docs.css +81 -0
  64. package/src/{@types/docs.ts → docs/types.ts} +8 -16
  65. package/src/{urls.ts → docs/urls.ts} +2 -16
  66. package/src/env.d.ts +1 -1
  67. package/src/integration.ts +15 -249
  68. package/src/{components → site/components}/Footer.astro +1 -1
  69. package/src/{components → site/components}/Header.astro +14 -11
  70. package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
  71. package/src/{components → site/components}/NavMobile.astro +1 -1
  72. package/src/{components → site/components}/SearchPalette.astro +160 -79
  73. package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
  74. package/src/site/config.ts +149 -0
  75. package/src/{infer.ts → site/infer.ts} +3 -3
  76. package/src/site/integration.ts +59 -0
  77. package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
  78. package/src/{locales.ts → site/locales.ts} +9 -1
  79. package/src/{docs-config.ts → site/runtime.ts} +1 -12
  80. package/src/site/search.ts +227 -0
  81. package/src/{social.ts → site/social.ts} +2 -2
  82. package/src/site/styles/components/header.css +31 -0
  83. package/src/{styles → site/styles}/components/search.css +21 -1
  84. package/src/{styles → site/styles}/components/shell.css +4 -0
  85. package/src/site/styles/components.css +10 -0
  86. package/src/{styles → site/styles}/global.css +4 -26
  87. package/src/{themes/index.ts → site/themes.ts} +1 -1
  88. package/src/{ui → site/ui}/en.ts +3 -5
  89. package/src/{ui → site/ui}/it.ts +2 -4
  90. package/src/site/urls.ts +14 -0
  91. package/src/{virtual.ts → site/virtual.ts} +12 -18
  92. package/src/styles.css +5 -0
  93. package/src/collection.ts +0 -97
  94. package/src/components/ScrollToTop.astro +0 -43
  95. package/src/components/docs/Breadcrumbs.astro +0 -36
  96. package/src/components/docs/DocsMobileNav.astro +0 -93
  97. package/src/components/docs/DocsPage.astro +0 -68
  98. package/src/components/docs/DocsSidebarLeft.astro +0 -82
  99. package/src/content.ts +0 -42
  100. package/src/navigation.ts +0 -97
  101. package/src/routes/docs-index.astro +0 -19
  102. package/src/routes/docs-slug.astro +0 -58
  103. package/src/styles/components/breadcrumbs.css +0 -13
  104. package/src/styles/components/docs-mobile-nav.css +0 -9
  105. package/src/styles/components/docs-page-nav.css +0 -31
  106. package/src/styles/components/docs-sidebar.css +0 -42
  107. package/src/styles/components/header.css +0 -20
  108. package/src/styles/components/page-actions.css +0 -13
  109. package/src/styles/components/scroll-to-top.css +0 -11
  110. package/src/styles/components/toc.css +0 -21
  111. package/src/styles/docs.css +0 -112
  112. package/src/utils/docs.ts +0 -300
  113. package/src/utils/endpoints.ts +0 -80
  114. package/src/utils/search.ts +0 -83
  115. /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
  116. /package/src/{styles → docs/styles}/jaamd.css +0 -0
  117. /package/src/{components → site/components}/Head.astro +0 -0
  118. /package/src/{components → site/components}/Nav.astro +0 -0
  119. /package/src/{components → site/components}/SocialLinks.astro +0 -0
  120. /package/src/{components → site/components}/locale-switcher.ts +0 -0
  121. /package/src/{fonts → site/fonts}/OFL.txt +0 -0
  122. /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
  123. /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
  124. /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
  125. /package/src/{fonts.ts → site/fonts.ts} +0 -0
  126. /package/src/{forges.ts → site/forges.ts} +0 -0
  127. /package/src/{utils → site}/helpers.ts +0 -0
  128. /package/src/{styles → site/styles}/components/footer.css +0 -0
  129. /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
  130. /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
  131. /package/src/{styles → site/styles}/components/nav.css +0 -0
  132. /package/src/{styles → site/styles}/components/not-found.css +0 -0
  133. /package/src/{styles → site/styles}/components/social-links.css +0 -0
  134. /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
  135. /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
  136. /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
  137. /package/src/{styles → site/styles}/tailwind.css +0 -0
  138. /package/src/{ui.ts → site/ui.ts} +0 -0
package/src/config.ts CHANGED
@@ -1,98 +1,12 @@
1
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
2
  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'
3
+ import { docsConfigShape, resolveDocsConfig, type DocsResolvedConfig } from './docs/config.ts'
4
+ import { resolveSiteConfig, siteConfigShape, type SiteResolvedConfig } from './site/config.ts'
5
+ import { inferRepo } from './site/infer.ts'
6
+ import { describeLocale } from './site/locales.ts'
12
7
 
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
- })
8
+ /** Site options, then docs options: the order the error for an unknown one lists them. */
9
+ export const jaadConfigSchema = z.object({ ...siteConfigShape, ...docsConfigShape })
96
10
 
97
11
  export type JaadConfig = z.output<typeof jaadConfigSchema>
98
12
 
@@ -106,39 +20,9 @@ export type JaadUserConfig = z.input<typeof jaadConfigSchema> & {
106
20
  astro?: AstroPassthroughConfig
107
21
  }
108
22
 
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)
23
+ /** What the components read: the site's fields at the top, the docs' under `docs`. */
24
+ export interface JaadResolvedConfig extends SiteResolvedConfig {
25
+ docs: DocsResolvedConfig
142
26
  }
143
27
 
144
28
  const OPTIONS = Object.keys(jaadConfigSchema.shape)
@@ -173,60 +57,22 @@ function parseConfig(options: JaadUserConfig): JaadConfig {
173
57
  export function resolveConfig(options: JaadUserConfig, cwd: string = process.cwd()): JaadResolvedConfig {
174
58
  const config = parseConfig(options)
175
59
  const repo = inferRepo(cwd)
176
- const locales = resolveLocales(config, cwd)
60
+ const { docs, locales } = resolveDocsConfig(config, config.lang, options.docsDir !== undefined, repo, cwd)
61
+ const {
62
+ docsDir: _docsDir,
63
+ routeBase: _routeBase,
64
+ sections: _sections,
65
+ editLink: _editLink,
66
+ locales: _locales,
67
+ ...siteOptions
68
+ } = config
177
69
 
178
70
  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),
71
+ ...siteOptions,
72
+ ...resolveSiteConfig(siteOptions, repo, cwd),
73
+ locales: locales.map(describeLocale),
188
74
  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,
75
+ docs,
230
76
  }
231
77
  }
232
78
 
@@ -234,3 +80,6 @@ export function resolveStylesheets(
234
80
  export function defineJaadConfig(config: JaadUserConfig): JaadUserConfig {
235
81
  return config
236
82
  }
83
+
84
+ export type { DocsResolvedConfig, DocsRoot, DocsSection } from './docs/config.ts'
85
+ export type { SiteResolvedConfig } from './site/config.ts'
@@ -1,5 +1,5 @@
1
- import type { DocsHeadings, DocsPageData } from './@types/docs.ts'
2
1
  import type { DocsNavigation } from './navigation.ts'
2
+ import type { DocsHeading, DocsPageData } from './types.ts'
3
3
 
4
4
  /** Data available to an advanced documentation frame. */
5
5
  export interface DocsFrameProps {
@@ -9,13 +9,15 @@ export interface DocsFrameProps {
9
9
  /** File-derived slug. The opening page keeps its named slug here. */
10
10
  slug: string
11
11
  title: string
12
+ /** Set when the docs are split into sections. */
13
+ section?: string
12
14
  chapter?: string
13
15
  /** Raw frontmatter. */
14
16
  data: DocsPageData
15
17
  }
16
- headings: DocsHeadings[]
18
+ headings: DocsHeading[]
17
19
  navigation: DocsNavigation
18
20
  }
19
21
 
20
- export type { DocsHeadings, DocsNavItem, DocsEntry, DocsPageData } from './@types/docs.ts'
21
- export type { DocsNavigation, NavSection } from './navigation.ts'
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>
@@ -1,6 +1,6 @@
1
1
  ---
2
- import type { DocsNavItem } from '../../@types/docs.ts'
3
- import { ui } from '../../docs-config.ts'
2
+ import type { DocsNavItem } from '../types.ts'
3
+ import { ui } from '../../site/runtime.ts'
4
4
 
5
5
  interface Props {
6
6
  locale?: string
@@ -18,7 +18,7 @@ const t = ui(locale)
18
18
  {previousPage && previousPage.title ? (
19
19
  <a href={previousPage.href} rel="prev" class="docs-page-nav-link">
20
20
  <svg
21
- class="docs-page-nav-icon docs-page-nav-icon-prev"
21
+ class="docs-page-nav-icon"
22
22
  fill="none"
23
23
  stroke="currentColor"
24
24
  viewBox="0 0 24 24"
@@ -36,7 +36,7 @@ const t = ui(locale)
36
36
  <a href={nextPage.href} rel="next" class="docs-page-nav-link">
37
37
  <span class="docs-page-nav-label">{nextPage.title}</span>
38
38
  <svg
39
- class="docs-page-nav-icon docs-page-nav-icon-next"
39
+ class="docs-page-nav-icon"
40
40
  fill="none"
41
41
  stroke="currentColor"
42
42
  viewBox="0 0 24 24"