@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.
- package/README.md +8 -2
- package/dist/jaad.css +232 -347
- 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 +8 -22
- package/package.json +10 -9
- package/site.ts +18 -1
- package/src/config.ts +25 -176
- package/src/{advanced.ts → docs/advanced.ts} +6 -4
- 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/{components/docs → docs/components}/DocsPageNav.astro +4 -4
- package/src/docs/components/DocsSidebarLeft.astro +53 -0
- package/src/{components/docs → docs/components}/PageActions.astro +10 -8
- package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
- package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
- 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/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
- package/src/{layouts → docs/layouts}/Docs.astro +44 -43
- 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/{locale-tree.ts → docs/locale-tree.ts} +1 -1
- 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/{routes → docs/routes}/404.astro +1 -1
- package/src/docs/routes/docs-index.astro +19 -0
- package/src/docs/routes/docs-slug.astro +54 -0
- package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
- package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
- package/src/{routes → docs/routes}/llms.txt.ts +1 -1
- package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
- package/src/{routes → docs/routes}/search-index.json.ts +1 -1
- 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-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/{@types/docs.ts → docs/types.ts} +8 -16
- package/src/{urls.ts → docs/urls.ts} +2 -16
- package/src/env.d.ts +1 -1
- package/src/integration.ts +15 -249
- package/src/{components → site/components}/Footer.astro +1 -1
- package/src/{components → site/components}/Header.astro +14 -11
- package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
- package/src/{components → site/components}/NavMobile.astro +1 -1
- package/src/{components → site/components}/SearchPalette.astro +160 -79
- package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
- package/src/site/config.ts +149 -0
- package/src/{infer.ts → site/infer.ts} +3 -3
- package/src/site/integration.ts +59 -0
- package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
- package/src/{locales.ts → site/locales.ts} +9 -1
- package/src/{docs-config.ts → site/runtime.ts} +1 -12
- package/src/site/search.ts +227 -0
- package/src/{social.ts → site/social.ts} +2 -2
- package/src/site/styles/components/header.css +31 -0
- package/src/{styles → site/styles}/components/search.css +21 -1
- package/src/{styles → site/styles}/components/shell.css +4 -0
- package/src/site/styles/components.css +10 -0
- package/src/{styles → site/styles}/global.css +4 -26
- package/src/{themes/index.ts → site/themes.ts} +1 -1
- package/src/{ui → site/ui}/en.ts +3 -5
- package/src/{ui → site/ui}/it.ts +2 -4
- package/src/site/urls.ts +14 -0
- package/src/{virtual.ts → site/virtual.ts} +12 -18
- package/src/styles.css +5 -0
- package/src/collection.ts +0 -97
- package/src/components/ScrollToTop.astro +0 -43
- package/src/components/docs/Breadcrumbs.astro +0 -36
- package/src/components/docs/DocsMobileNav.astro +0 -93
- package/src/components/docs/DocsPage.astro +0 -68
- package/src/components/docs/DocsSidebarLeft.astro +0 -82
- package/src/content.ts +0 -42
- package/src/navigation.ts +0 -97
- package/src/routes/docs-index.astro +0 -19
- package/src/routes/docs-slug.astro +0 -58
- package/src/styles/components/breadcrumbs.css +0 -13
- package/src/styles/components/docs-mobile-nav.css +0 -9
- package/src/styles/components/docs-page-nav.css +0 -31
- package/src/styles/components/docs-sidebar.css +0 -42
- package/src/styles/components/header.css +0 -20
- package/src/styles/components/page-actions.css +0 -13
- package/src/styles/components/scroll-to-top.css +0 -11
- package/src/styles/components/toc.css +0 -21
- package/src/styles/docs.css +0 -112
- package/src/utils/docs.ts +0 -300
- package/src/utils/endpoints.ts +0 -80
- package/src/utils/search.ts +0 -83
- /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
- /package/src/{styles → docs/styles}/jaamd.css +0 -0
- /package/src/{components → site/components}/Head.astro +0 -0
- /package/src/{components → site/components}/Nav.astro +0 -0
- /package/src/{components → site/components}/SocialLinks.astro +0 -0
- /package/src/{components → site/components}/locale-switcher.ts +0 -0
- /package/src/{fonts → site/fonts}/OFL.txt +0 -0
- /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
- /package/src/{fonts.ts → site/fonts.ts} +0 -0
- /package/src/{forges.ts → site/forges.ts} +0 -0
- /package/src/{utils → site}/helpers.ts +0 -0
- /package/src/{styles → site/styles}/components/footer.css +0 -0
- /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
- /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
- /package/src/{styles → site/styles}/components/nav.css +0 -0
- /package/src/{styles → site/styles}/components/not-found.css +0 -0
- /package/src/{styles → site/styles}/components/social-links.css +0 -0
- /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
- /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
- /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
- /package/src/{styles → site/styles}/tailwind.css +0 -0
- /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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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
|
-
...
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
18
|
+
headings: DocsHeading[]
|
|
17
19
|
navigation: DocsNavigation
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export type {
|
|
21
|
-
export type {
|
|
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 '
|
|
3
|
-
import { ui } from '../../
|
|
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
|
|
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
|
|
39
|
+
class="docs-page-nav-icon"
|
|
40
40
|
fill="none"
|
|
41
41
|
stroke="currentColor"
|
|
42
42
|
viewBox="0 0 24 24"
|