@jaamd/jaad 0.0.0-stage → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/dist/jaad.css +1446 -0
- package/index.ts +45 -0
- package/package.json +70 -3
- package/site.ts +19 -0
- package/src/@types/docs.ts +47 -0
- package/src/advanced.ts +21 -0
- package/src/collection.ts +97 -0
- package/src/components/Footer.astro +18 -0
- package/src/components/Head.astro +119 -0
- package/src/components/Header.astro +47 -0
- package/src/components/LocaleSwitcher.astro +111 -0
- package/src/components/Nav.astro +22 -0
- package/src/components/NavMobile.astro +105 -0
- package/src/components/ScrollToTop.astro +43 -0
- package/src/components/SearchPalette.astro +328 -0
- package/src/components/SocialLinks.astro +27 -0
- package/src/components/ThemeSwitcher.astro +55 -0
- package/src/components/docs/Breadcrumbs.astro +36 -0
- package/src/components/docs/DocsMobileNav.astro +93 -0
- package/src/components/docs/DocsPage.astro +68 -0
- package/src/components/docs/DocsPageNav.astro +53 -0
- package/src/components/docs/DocsSidebarLeft.astro +82 -0
- package/src/components/docs/PageActions.astro +81 -0
- package/src/components/docs/StructuredData.astro +55 -0
- package/src/components/docs/TableOfContents.astro +73 -0
- package/src/components/locale-switcher.ts +6 -0
- package/src/config.ts +236 -0
- package/src/content.ts +42 -0
- package/src/docs-config.ts +30 -0
- package/src/env.d.ts +13 -0
- package/src/fonts/OFL.txt +94 -0
- package/src/fonts/inter.woff2 +0 -0
- package/src/fonts/merriweather-italic.woff2 +0 -0
- package/src/fonts/merriweather.woff2 +0 -0
- package/src/fonts.ts +29 -0
- package/src/forges.ts +60 -0
- package/src/infer.ts +75 -0
- package/src/integration.ts +270 -0
- package/src/layouts/DefaultDocsFrame.astro +31 -0
- package/src/layouts/Docs.astro +168 -0
- package/src/layouts/Page.astro +38 -0
- package/src/locale-tree.ts +51 -0
- package/src/locales.ts +191 -0
- package/src/navigation.ts +97 -0
- package/src/routes/404.astro +16 -0
- package/src/routes/docs-index.astro +19 -0
- package/src/routes/docs-slug.astro +58 -0
- package/src/routes/docs-slug.md.ts +37 -0
- package/src/routes/llms-locale.txt.ts +8 -0
- package/src/routes/llms.txt.ts +4 -0
- package/src/routes/search-index-locale.json.ts +8 -0
- package/src/routes/search-index.json.ts +4 -0
- package/src/social.ts +44 -0
- package/src/styles/components/breadcrumbs.css +13 -0
- package/src/styles/components/docs-mobile-nav.css +9 -0
- package/src/styles/components/docs-page-nav.css +31 -0
- package/src/styles/components/docs-page.css +9 -0
- package/src/styles/components/docs-sidebar.css +42 -0
- package/src/styles/components/footer.css +8 -0
- package/src/styles/components/header.css +20 -0
- package/src/styles/components/locale-switcher.css +20 -0
- package/src/styles/components/nav-mobile.css +15 -0
- package/src/styles/components/nav.css +11 -0
- package/src/styles/components/not-found.css +11 -0
- package/src/styles/components/page-actions.css +13 -0
- package/src/styles/components/scroll-to-top.css +11 -0
- package/src/styles/components/search.css +98 -0
- package/src/styles/components/shell.css +73 -0
- package/src/styles/components/social-links.css +8 -0
- package/src/styles/components/theme-switcher.css +14 -0
- package/src/styles/components/toc.css +21 -0
- package/src/styles/docs.css +112 -0
- package/src/styles/global.css +162 -0
- package/src/styles/jaamd-forward.css +16 -0
- package/src/styles/jaamd-reverse.css +18 -0
- package/src/styles/jaamd.css +11 -0
- package/src/styles/tailwind.css +30 -0
- package/src/themes/index.ts +26 -0
- package/src/ui/en.ts +29 -0
- package/src/ui/it.ts +30 -0
- package/src/ui.ts +49 -0
- package/src/urls.ts +49 -0
- package/src/utils/docs.ts +300 -0
- package/src/utils/endpoints.ts +80 -0
- package/src/utils/helpers.ts +4 -0
- package/src/utils/search.ts +83 -0
- package/src/virtual.ts +48 -0
package/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro'
|
|
2
|
+
import sitemap from '@astrojs/sitemap'
|
|
3
|
+
import jaamd from '@jaamd/jaamd'
|
|
4
|
+
import { defineConfig } from 'astro/config'
|
|
5
|
+
import { resolveConfig, resolveStylesheets, type JaadUserConfig } from './src/config.ts'
|
|
6
|
+
import { createCoreIntegration, createIntegrationState, createSitemapFilter } from './src/integration.ts'
|
|
7
|
+
|
|
8
|
+
/** The whole set: Astro flattens nested arrays in `integrations`. */
|
|
9
|
+
export default function jaad(options: JaadUserConfig): AstroIntegration[] {
|
|
10
|
+
const config = resolveConfig(options)
|
|
11
|
+
const css = resolveStylesheets(config)
|
|
12
|
+
const state = createIntegrationState()
|
|
13
|
+
const core = createCoreIntegration(config, css, state, Boolean(options.site))
|
|
14
|
+
|
|
15
|
+
// Without `site` the sitemap integration can only warn and skip.
|
|
16
|
+
return [
|
|
17
|
+
jaamd({ theme: config.shiki }),
|
|
18
|
+
...(options.site
|
|
19
|
+
? [
|
|
20
|
+
sitemap({
|
|
21
|
+
filter: createSitemapFilter(config.docsBase, state, config.defaultLocale),
|
|
22
|
+
}),
|
|
23
|
+
]
|
|
24
|
+
: []),
|
|
25
|
+
core,
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The whole Astro config for a JAAD site. `astro` is merged in, with its
|
|
30
|
+
* integrations appended rather than replacing ours. */
|
|
31
|
+
export function defineJaadSite(options: JaadUserConfig) {
|
|
32
|
+
const { site, base, astro } = options
|
|
33
|
+
const extra = astro ?? {}
|
|
34
|
+
const extraIntegrations = Array.isArray(extra.integrations) ? (extra.integrations as AstroIntegration[]) : []
|
|
35
|
+
|
|
36
|
+
return defineConfig({
|
|
37
|
+
...(site ? { site } : {}),
|
|
38
|
+
...(base ? { base } : {}),
|
|
39
|
+
...extra,
|
|
40
|
+
integrations: [...jaad(options), ...extraIntegrations],
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { defineJaadConfig } from './src/config.ts'
|
|
45
|
+
export type { JaadUserConfig, JaadConfig, JaadResolvedConfig } from './src/config.ts'
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaamd/jaad",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Turn a folder of markdown into a documentation site. An Astro integration.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"astro-integration",
|
|
8
|
+
"docs",
|
|
9
|
+
"documentation",
|
|
10
|
+
"markdown",
|
|
11
|
+
"withastro"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://jaamd.org/docs/jaad",
|
|
14
|
+
"bugs": "https://github.com/jaamd/jaamd/issues",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Kasui92 <info@lucapattocchio.dev>",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/jaamd/jaamd.git",
|
|
20
|
+
"directory": "packages/jaad"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.ts",
|
|
24
|
+
"site.ts",
|
|
25
|
+
"src",
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./index.ts",
|
|
33
|
+
"./advanced": "./src/advanced.ts",
|
|
34
|
+
"./content": "./src/content.ts",
|
|
35
|
+
"./layouts/Page.astro": "./src/layouts/Page.astro",
|
|
36
|
+
"./site": "./site.ts"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@astrojs/sitemap": "^3.7.4",
|
|
43
|
+
"@jaamd/jaamd": "^0.12.0",
|
|
44
|
+
"github-slugger": "^2.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
48
|
+
"tailwindcss": "^4.3.3"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
52
|
+
"astro": ">=7.0.0 <8.0.0",
|
|
53
|
+
"tailwindcss": "^4.3.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"tailwindcss": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@tailwindcss/vite": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=22.12.0"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tailwindcss -i ./src/styles/global.css -o ./dist/jaad.css",
|
|
68
|
+
"check": "astro check && tsc --noEmit -p tests",
|
|
69
|
+
"test": "node --test \"tests/unit/**/*.test.ts\" \"tests/unit/**/*.test.mjs\"",
|
|
70
|
+
"test:consumer": "node --test \"tests/consumer/**/*.test.mjs\"",
|
|
71
|
+
"test:build": "pnpm --filter jaad-fixture build && node --test \"tests/build/**/*.test.mjs\""
|
|
72
|
+
}
|
|
6
73
|
}
|
package/site.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JaadUserConfig } from './src/config.ts'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { defineJaadSite } from './index.ts'
|
|
6
|
+
|
|
7
|
+
const CANDIDATES = ['jaad.config.ts', 'jaad.config.mjs', 'jaad.config.js']
|
|
8
|
+
|
|
9
|
+
const file = CANDIDATES.map((n) => join(process.cwd(), n)).find(existsSync)
|
|
10
|
+
if (!file) {
|
|
11
|
+
throw new Error(`jaad: no config found. Create one of ${CANDIDATES.join(', ')} in the project root.`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Unanalysable on purpose: a static path would make Vite warn on every build.
|
|
15
|
+
const loaded = (await import(/* @vite-ignore */ pathToFileURL(file).href)) as {
|
|
16
|
+
default: JaadUserConfig
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineJaadSite(loaded.default)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface ParsedDocsCollectionId {
|
|
2
|
+
slug: string
|
|
3
|
+
orderChapter?: number
|
|
4
|
+
chapter?: string
|
|
5
|
+
order: number
|
|
6
|
+
title: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DocsNavItem {
|
|
10
|
+
title: string
|
|
11
|
+
chapter: string | undefined
|
|
12
|
+
primaryOrder: number
|
|
13
|
+
href: string
|
|
14
|
+
isActive: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DocsHeadings {
|
|
18
|
+
depth: 2 | 3
|
|
19
|
+
slug: string
|
|
20
|
+
text: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolved frontmatter. Mirrors the schema in content.ts. */
|
|
24
|
+
export interface DocsPageData {
|
|
25
|
+
title?: string
|
|
26
|
+
description?: string
|
|
27
|
+
label?: string
|
|
28
|
+
translationKey?: string
|
|
29
|
+
order?: number
|
|
30
|
+
draft: boolean
|
|
31
|
+
keywords: string[]
|
|
32
|
+
author?: string
|
|
33
|
+
ogImage?: string | false
|
|
34
|
+
lastUpdated?: Date
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The shape jaad requires of the docs collection. Astro generates collection
|
|
38
|
+
* types in the consumer project, so the package states its own contract. */
|
|
39
|
+
export interface DocsEntry {
|
|
40
|
+
id: string
|
|
41
|
+
/** Set when the docs are localised; the id without its locale directory. */
|
|
42
|
+
localeId?: string
|
|
43
|
+
/** Set when the docs are localised. */
|
|
44
|
+
locale?: string
|
|
45
|
+
body?: string
|
|
46
|
+
data: DocsPageData
|
|
47
|
+
}
|
package/src/advanced.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DocsHeadings, DocsPageData } from './@types/docs.ts'
|
|
2
|
+
import type { DocsNavigation } from './navigation.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
|
+
chapter?: string
|
|
13
|
+
/** Raw frontmatter. */
|
|
14
|
+
data: DocsPageData
|
|
15
|
+
}
|
|
16
|
+
headings: DocsHeadings[]
|
|
17
|
+
navigation: DocsNavigation
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type { DocsHeadings, DocsNavItem, DocsEntry, DocsPageData } from './@types/docs.ts'
|
|
21
|
+
export type { DocsNavigation, NavSection } from './navigation.ts'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { DocsEntry } from './@types/docs.ts'
|
|
2
|
+
import type { Locale } from './locales.ts'
|
|
3
|
+
import { getCollection } from 'astro:content'
|
|
4
|
+
import { DEFAULT_LOCALE, LOCALES, docsUrls, prefixedCodes } from './docs-config.ts'
|
|
5
|
+
import { docsPageHref } from './urls.ts'
|
|
6
|
+
import {
|
|
7
|
+
docTranslationKey,
|
|
8
|
+
getCleanSlug,
|
|
9
|
+
routedId,
|
|
10
|
+
sortDocPages,
|
|
11
|
+
splitDocLocale,
|
|
12
|
+
validateDocsStructure,
|
|
13
|
+
} from './utils/docs.ts'
|
|
14
|
+
|
|
15
|
+
const cached = new Map<string, DocsEntry[]>()
|
|
16
|
+
// Keyed on the array: the same object in production, a fresh one in dev.
|
|
17
|
+
const keyIndex = new WeakMap<DocsEntry[], Map<string, number>>()
|
|
18
|
+
|
|
19
|
+
function indexOfKey(pages: DocsEntry[], key: string): number {
|
|
20
|
+
let index = keyIndex.get(pages)
|
|
21
|
+
if (!index) {
|
|
22
|
+
index = new Map(pages.map((page, at) => [docTranslationKey(page), at]))
|
|
23
|
+
keyIndex.set(pages, index)
|
|
24
|
+
}
|
|
25
|
+
return index.get(key) ?? -1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CODES = LOCALES.map((locale) => locale.code)
|
|
29
|
+
|
|
30
|
+
/** Attaches the locale and the id routing reads, leaving `id` the real
|
|
31
|
+
* collection id: the edit link points at the file on disk. */
|
|
32
|
+
function localise(page: DocsEntry): DocsEntry {
|
|
33
|
+
if (CODES.length === 0) return page
|
|
34
|
+
const split = splitDocLocale(page.id, CODES)
|
|
35
|
+
return { ...page, locale: split.locale ?? undefined, localeId: split.id }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The pages of one locale, or every page when the docs are not localised.
|
|
39
|
+
* Cached in production only: in dev the module would go stale on edit. */
|
|
40
|
+
export async function getSortedDocsPages(locale: string = DEFAULT_LOCALE): Promise<DocsEntry[]> {
|
|
41
|
+
const key = CODES.length > 0 ? locale : ''
|
|
42
|
+
const hit = cached.get(key)
|
|
43
|
+
if (import.meta.env.PROD && hit) return hit
|
|
44
|
+
|
|
45
|
+
const pages = ((await getCollection('docsPages')) as unknown as DocsEntry[])
|
|
46
|
+
.map(localise)
|
|
47
|
+
.filter((page) => key === '' || page.locale === key)
|
|
48
|
+
|
|
49
|
+
// Validated before drafts are dropped, so a draft's slug conflict is an error.
|
|
50
|
+
validateDocsStructure(pages, key || undefined)
|
|
51
|
+
const sorted = sortDocPages(import.meta.env.PROD ? pages.filter((page) => !page.data.draft) : pages)
|
|
52
|
+
if (import.meta.env.PROD) cached.set(key, sorted)
|
|
53
|
+
return sorted
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LocaleAlternate {
|
|
57
|
+
locale: Locale
|
|
58
|
+
href: string
|
|
59
|
+
/** False when this locale has no such page and the href is its opening one. */
|
|
60
|
+
translated: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Where a page lives in every locale, paired by `translationKey` or by slug.
|
|
64
|
+
* An untranslated page falls back to that locale's opening page. Empty asks
|
|
65
|
+
* for the opening pages themselves. */
|
|
66
|
+
export async function getLocaleAlternates(key: string): Promise<LocaleAlternate[]> {
|
|
67
|
+
const alternates: LocaleAlternate[] = []
|
|
68
|
+
|
|
69
|
+
for (const locale of LOCALES) {
|
|
70
|
+
const pages = await getSortedDocsPages(locale.code)
|
|
71
|
+
// Every page drafted leaves a locale with nothing to link to.
|
|
72
|
+
if (pages.length === 0) continue
|
|
73
|
+
|
|
74
|
+
const index = key ? indexOfKey(pages, key) : 0
|
|
75
|
+
const match = index >= 0 ? pages[index] : undefined
|
|
76
|
+
|
|
77
|
+
alternates.push({
|
|
78
|
+
locale,
|
|
79
|
+
translated: Boolean(match),
|
|
80
|
+
href: docsPageHref(match && index > 0 ? getCleanSlug(routedId(match)) : '', docsUrls(locale.code)),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return alternates
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The prefixed locales that have something to serve. */
|
|
88
|
+
export async function getServedLocales(): Promise<string[]> {
|
|
89
|
+
const served: string[] = []
|
|
90
|
+
|
|
91
|
+
for (const code of prefixedCodes()) {
|
|
92
|
+
const pages = await getSortedDocsPages(code)
|
|
93
|
+
if (pages.length > 0) served.push(code)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return served
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
|
|
4
|
+
const CREDIT = 'Built with <a href="https://jaamd.org/docs/jaad" rel="noopener">JAAD</a>.'
|
|
5
|
+
|
|
6
|
+
// Unset is the credit alone; `footer: false` removes it.
|
|
7
|
+
const line = config.footer === undefined ? ':credit' : config.footer
|
|
8
|
+
const html = line === false ? null : line.replaceAll(':credit', CREDIT)
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
{html && (
|
|
12
|
+
<footer class="jaad-chrome jaad-footer" aria-labelledby="footer-heading">
|
|
13
|
+
<h2 class="jaad-sr-only" id="footer-heading">
|
|
14
|
+
Site footer
|
|
15
|
+
</h2>
|
|
16
|
+
<p class="jaad-footer-credit" set:html={html} />
|
|
17
|
+
</footer>
|
|
18
|
+
)}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Font } from 'astro:assets'
|
|
3
|
+
import config from 'virtual:jaad/config'
|
|
4
|
+
import { openGraphLocale } from '../locales.ts'
|
|
5
|
+
import { withDeploymentBase } from '../urls.ts'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string
|
|
9
|
+
description: string
|
|
10
|
+
/** Page frontmatter; each field overrides the site's. */
|
|
11
|
+
keywords?: string[]
|
|
12
|
+
author?: string
|
|
13
|
+
image?: string | false
|
|
14
|
+
modified?: Date
|
|
15
|
+
/** BCP-47 tag of this page's locale. */
|
|
16
|
+
language?: string
|
|
17
|
+
/** Every locale that has this page, for hreflang and og:locale:alternate. */
|
|
18
|
+
alternates?: { code: string; tag: string; href: string }[]
|
|
19
|
+
/** Where a reader with no matching language should land. */
|
|
20
|
+
xDefault?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
keywords = [],
|
|
27
|
+
author,
|
|
28
|
+
image,
|
|
29
|
+
modified,
|
|
30
|
+
language = config.lang,
|
|
31
|
+
alternates = [],
|
|
32
|
+
xDefault,
|
|
33
|
+
} = Astro.props
|
|
34
|
+
const canonical = Astro.site ? Astro.url.href : null
|
|
35
|
+
|
|
36
|
+
// Open Graph rejects relative paths, so these have to be absolute.
|
|
37
|
+
const resolvedImage = image ?? config.ogImage
|
|
38
|
+
const ogImage =
|
|
39
|
+
resolvedImage === false
|
|
40
|
+
? null
|
|
41
|
+
: /^https?:\/\//.test(resolvedImage)
|
|
42
|
+
? resolvedImage
|
|
43
|
+
: Astro.site
|
|
44
|
+
? new URL(withDeploymentBase(resolvedImage, import.meta.env.BASE_URL), Astro.site).href
|
|
45
|
+
: null
|
|
46
|
+
const favicon = config.favicon ? withDeploymentBase(config.favicon, import.meta.env.BASE_URL) : null
|
|
47
|
+
|
|
48
|
+
// Applied before the first paint, so the page never flashes the wrong theme.
|
|
49
|
+
const themeScript = `(function () {
|
|
50
|
+
function isDark() {
|
|
51
|
+
var t = localStorage.getItem("theme");
|
|
52
|
+
return (
|
|
53
|
+
t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function applyTheme(root) {
|
|
57
|
+
if (isDark()) {
|
|
58
|
+
root.classList.add("dark");
|
|
59
|
+
} else {
|
|
60
|
+
root.classList.remove("dark");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
applyTheme(document.documentElement);
|
|
64
|
+
document.addEventListener("astro:before-swap", function (e) {
|
|
65
|
+
applyTheme(e.newDocument.documentElement);
|
|
66
|
+
});
|
|
67
|
+
document.addEventListener("astro:after-swap", function () {
|
|
68
|
+
applyTheme(document.documentElement);
|
|
69
|
+
});
|
|
70
|
+
})();`
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
<meta charset="UTF-8" />
|
|
74
|
+
<meta name="viewport" content="width=device-width" />
|
|
75
|
+
<meta name="description" content={description} />
|
|
76
|
+
{keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
|
|
77
|
+
{author && <meta name="author" content={author} />}
|
|
78
|
+
<title>{title}</title>
|
|
79
|
+
{canonical && <link rel="canonical" href={canonical} />}
|
|
80
|
+
{Astro.site &&
|
|
81
|
+
alternates.map((alternate) => (
|
|
82
|
+
<link rel="alternate" hreflang={alternate.tag} href={new URL(alternate.href, Astro.site).href} />
|
|
83
|
+
))}
|
|
84
|
+
{Astro.site && xDefault && <link rel="alternate" hreflang="x-default" href={new URL(xDefault, Astro.site).href} />}
|
|
85
|
+
|
|
86
|
+
{favicon && <link rel="icon" href={favicon} />}
|
|
87
|
+
|
|
88
|
+
<Font cssVariable="--font-inter" preload />
|
|
89
|
+
<Font cssVariable="--font-merriweather" preload />
|
|
90
|
+
|
|
91
|
+
{canonical && <meta property="og:url" content={canonical} />}
|
|
92
|
+
<meta property="og:type" content="website" />
|
|
93
|
+
<meta property="og:site_name" content={config.title} />
|
|
94
|
+
<meta property="og:locale" content={openGraphLocale(language)} />
|
|
95
|
+
{alternates
|
|
96
|
+
.filter((alternate) => alternate.tag !== language)
|
|
97
|
+
.map((alternate) => (
|
|
98
|
+
<meta property="og:locale:alternate" content={openGraphLocale(alternate.tag)} />
|
|
99
|
+
))}
|
|
100
|
+
<meta property="og:title" content={title} />
|
|
101
|
+
<meta property="og:description" content={description} />
|
|
102
|
+
{modified && <meta property="article:modified_time" content={modified.toISOString()} />}
|
|
103
|
+
{ogImage && <meta property="og:image" content={ogImage} />}
|
|
104
|
+
|
|
105
|
+
<meta name="twitter:card" content={ogImage ? 'summary_large_image' : 'summary'} />
|
|
106
|
+
{canonical && <meta property="twitter:domain" content={Astro.url.hostname} />}
|
|
107
|
+
{canonical && <meta property="twitter:url" content={canonical} />}
|
|
108
|
+
<meta name="twitter:title" content={title} />
|
|
109
|
+
<meta name="twitter:description" content={description} />
|
|
110
|
+
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
|
111
|
+
|
|
112
|
+
<meta name="generator" content={Astro.generator} />
|
|
113
|
+
|
|
114
|
+
{config.head.map((tag) => {
|
|
115
|
+
const Tag = tag.tag as keyof HTMLElementTagNameMap
|
|
116
|
+
return <Tag {...tag.attrs} set:html={tag.content ?? ''} />
|
|
117
|
+
})}
|
|
118
|
+
|
|
119
|
+
{config.appearance === 'auto' && <script is:inline set:html={themeScript} />}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { getLocaleAlternates, type LocaleAlternate } from '../collection.ts'
|
|
4
|
+
import { DEFAULT_LOCALE, ui } from '../docs-config.ts'
|
|
5
|
+
import { withDeploymentBase } from '../urls.ts'
|
|
6
|
+
import LocaleSwitcher from './LocaleSwitcher.astro'
|
|
7
|
+
import Nav from './Nav.astro'
|
|
8
|
+
import NavMobile from './NavMobile.astro'
|
|
9
|
+
import SearchPalette from './SearchPalette.astro'
|
|
10
|
+
import SocialLinks from './SocialLinks.astro'
|
|
11
|
+
import ThemeSwitcher from './ThemeSwitcher.astro'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Whether this page is part of the docs. */
|
|
15
|
+
inDocs?: boolean
|
|
16
|
+
locale?: string
|
|
17
|
+
alternates?: LocaleAlternate[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { inDocs = false, locale = DEFAULT_LOCALE } = Astro.props
|
|
21
|
+
// A page of the reader's own has no translations: offer each locale's opener.
|
|
22
|
+
const alternates = Astro.props.alternates ?? (await getLocaleAlternates(''))
|
|
23
|
+
const t = ui(locale)
|
|
24
|
+
const homeHref = withDeploymentBase('/', import.meta.env.BASE_URL)
|
|
25
|
+
const logoSrc = config.logo ? withDeploymentBase(config.logo, import.meta.env.BASE_URL) : null
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<header class="jaad-chrome" transition:persist>
|
|
29
|
+
<div class="jaad-header-inner">
|
|
30
|
+
<div class="jaad-header-row">
|
|
31
|
+
<span class="jaad-brand font-brand">
|
|
32
|
+
<a href={homeHref} title={t('home')} class="jaad-brand-link">
|
|
33
|
+
{logoSrc ? <img src={logoSrc} alt={config.title} class="jaad-logo" /> : config.title}
|
|
34
|
+
</a>
|
|
35
|
+
</span>
|
|
36
|
+
|
|
37
|
+
<div class="jaad-header-actions">
|
|
38
|
+
<SearchPalette inDocs={inDocs} locale={locale} />
|
|
39
|
+
<Nav />
|
|
40
|
+
<SocialLinks />
|
|
41
|
+
{config.appearance === 'auto' && <ThemeSwitcher {locale} />}
|
|
42
|
+
{alternates.length > 1 && <LocaleSwitcher {alternates} current={locale} />}
|
|
43
|
+
<NavMobile {locale} />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</header>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { LocaleAlternate } from '../collection.ts'
|
|
3
|
+
import { ui } from '../docs-config.ts'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
alternates: LocaleAlternate[]
|
|
7
|
+
current: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { alternates, current } = Astro.props
|
|
11
|
+
const active = alternates.find((entry) => entry.locale.code === current)
|
|
12
|
+
const t = ui(current)
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<jaad-locale-switcher class="jaad-locale-switcher">
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
data-trigger
|
|
19
|
+
aria-expanded="false"
|
|
20
|
+
aria-haspopup="true"
|
|
21
|
+
aria-controls="jaad-locale-menu"
|
|
22
|
+
aria-label={t('locale.change')}
|
|
23
|
+
title={t('locale.change')}
|
|
24
|
+
class="jaad-icon-button jaad-locale-trigger"
|
|
25
|
+
>
|
|
26
|
+
{active?.locale.flag && <span aria-hidden="true">{active.locale.flag}</span>}
|
|
27
|
+
<span class="jaad-locale-code">{current}</span>
|
|
28
|
+
<svg class="jaad-locale-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
29
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
|
|
33
|
+
<div id="jaad-locale-menu" data-menu hidden class="jaad-menu jaad-locale-menu">
|
|
34
|
+
{alternates.map((entry) => (
|
|
35
|
+
<a
|
|
36
|
+
href={entry.href}
|
|
37
|
+
hreflang={entry.locale.tag}
|
|
38
|
+
lang={entry.locale.tag}
|
|
39
|
+
aria-current={entry.locale.code === current ? 'true' : undefined}
|
|
40
|
+
class="jaad-menu-item jaad-locale-item"
|
|
41
|
+
>
|
|
42
|
+
{entry.locale.flag && <span aria-hidden="true">{entry.locale.flag}</span>}
|
|
43
|
+
<span>{entry.locale.name}</span>
|
|
44
|
+
</a>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</jaad-locale-switcher>
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
import { nextSwitcherMarkup } from './locale-switcher.ts'
|
|
51
|
+
|
|
52
|
+
class JaadLocaleSwitcher extends HTMLElement {
|
|
53
|
+
#onDocumentPointerDown = (event: PointerEvent) => {
|
|
54
|
+
if (!this.contains(event.target as Node)) this.#close()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#onKeyDown = (event: KeyboardEvent) => {
|
|
58
|
+
if (event.key === 'Escape') this.#close()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The header persists across navigations; its links must not. */
|
|
62
|
+
#onBeforeSwap = (event: Event) => {
|
|
63
|
+
const markup = nextSwitcherMarkup((event as Event & { newDocument: Document }).newDocument)
|
|
64
|
+
if (markup !== null) this.innerHTML = markup
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Delegated: #onBeforeSwap replaces the button this would bind to. */
|
|
68
|
+
#onClick = (event: MouseEvent) => {
|
|
69
|
+
const target = event.target as Element | null
|
|
70
|
+
if (!target?.closest('[data-trigger]')) return
|
|
71
|
+
if (this.#menu()?.hidden) this.#open()
|
|
72
|
+
else this.#close()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
connectedCallback() {
|
|
76
|
+
this.addEventListener('click', this.#onClick)
|
|
77
|
+
document.addEventListener('pointerdown', this.#onDocumentPointerDown)
|
|
78
|
+
document.addEventListener('keydown', this.#onKeyDown)
|
|
79
|
+
document.addEventListener('astro:before-swap', this.#onBeforeSwap)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
disconnectedCallback() {
|
|
83
|
+
this.removeEventListener('click', this.#onClick)
|
|
84
|
+
document.removeEventListener('pointerdown', this.#onDocumentPointerDown)
|
|
85
|
+
document.removeEventListener('keydown', this.#onKeyDown)
|
|
86
|
+
document.removeEventListener('astro:before-swap', this.#onBeforeSwap)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#menu() {
|
|
90
|
+
return this.querySelector<HTMLElement>('[data-menu]')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#open() {
|
|
94
|
+
const menu = this.#menu()
|
|
95
|
+
if (menu) menu.hidden = false
|
|
96
|
+
this.querySelector('[data-trigger]')?.setAttribute('aria-expanded', 'true')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#close() {
|
|
100
|
+
const menu = this.#menu()
|
|
101
|
+
if (menu) menu.hidden = true
|
|
102
|
+
const trigger = this.querySelector<HTMLElement>('[data-trigger]')
|
|
103
|
+
trigger?.setAttribute('aria-expanded', 'false')
|
|
104
|
+
if (this.contains(document.activeElement)) trigger?.focus()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!customElements.get('jaad-locale-switcher')) {
|
|
109
|
+
customElements.define('jaad-locale-switcher', JaadLocaleSwitcher)
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from 'virtual:jaad/config'
|
|
3
|
+
import { withDeploymentBase } from '../urls.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
{config.nav.length > 0 && (
|
|
7
|
+
<nav class="jaad-nav">
|
|
8
|
+
<ul class="jaad-nav-list">
|
|
9
|
+
{config.nav.map((link) => (
|
|
10
|
+
<li>
|
|
11
|
+
<a
|
|
12
|
+
href={withDeploymentBase(link.href, import.meta.env.BASE_URL)}
|
|
13
|
+
class="jaad-nav-link"
|
|
14
|
+
{...(link.href.startsWith('http') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
15
|
+
>
|
|
16
|
+
{link.label}
|
|
17
|
+
</a>
|
|
18
|
+
</li>
|
|
19
|
+
))}
|
|
20
|
+
</ul>
|
|
21
|
+
</nav>
|
|
22
|
+
)}
|