@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -2
  3. package/dist/jaad.css +1446 -0
  4. package/index.ts +45 -0
  5. package/package.json +70 -3
  6. package/site.ts +19 -0
  7. package/src/@types/docs.ts +47 -0
  8. package/src/advanced.ts +21 -0
  9. package/src/collection.ts +97 -0
  10. package/src/components/Footer.astro +18 -0
  11. package/src/components/Head.astro +119 -0
  12. package/src/components/Header.astro +47 -0
  13. package/src/components/LocaleSwitcher.astro +111 -0
  14. package/src/components/Nav.astro +22 -0
  15. package/src/components/NavMobile.astro +105 -0
  16. package/src/components/ScrollToTop.astro +43 -0
  17. package/src/components/SearchPalette.astro +328 -0
  18. package/src/components/SocialLinks.astro +27 -0
  19. package/src/components/ThemeSwitcher.astro +55 -0
  20. package/src/components/docs/Breadcrumbs.astro +36 -0
  21. package/src/components/docs/DocsMobileNav.astro +93 -0
  22. package/src/components/docs/DocsPage.astro +68 -0
  23. package/src/components/docs/DocsPageNav.astro +53 -0
  24. package/src/components/docs/DocsSidebarLeft.astro +82 -0
  25. package/src/components/docs/PageActions.astro +81 -0
  26. package/src/components/docs/StructuredData.astro +55 -0
  27. package/src/components/docs/TableOfContents.astro +73 -0
  28. package/src/components/locale-switcher.ts +6 -0
  29. package/src/config.ts +236 -0
  30. package/src/content.ts +42 -0
  31. package/src/docs-config.ts +30 -0
  32. package/src/env.d.ts +13 -0
  33. package/src/fonts/OFL.txt +94 -0
  34. package/src/fonts/inter.woff2 +0 -0
  35. package/src/fonts/merriweather-italic.woff2 +0 -0
  36. package/src/fonts/merriweather.woff2 +0 -0
  37. package/src/fonts.ts +29 -0
  38. package/src/forges.ts +60 -0
  39. package/src/infer.ts +75 -0
  40. package/src/integration.ts +270 -0
  41. package/src/layouts/DefaultDocsFrame.astro +31 -0
  42. package/src/layouts/Docs.astro +168 -0
  43. package/src/layouts/Page.astro +38 -0
  44. package/src/locale-tree.ts +51 -0
  45. package/src/locales.ts +191 -0
  46. package/src/navigation.ts +97 -0
  47. package/src/routes/404.astro +16 -0
  48. package/src/routes/docs-index.astro +19 -0
  49. package/src/routes/docs-slug.astro +58 -0
  50. package/src/routes/docs-slug.md.ts +37 -0
  51. package/src/routes/llms-locale.txt.ts +8 -0
  52. package/src/routes/llms.txt.ts +4 -0
  53. package/src/routes/search-index-locale.json.ts +8 -0
  54. package/src/routes/search-index.json.ts +4 -0
  55. package/src/social.ts +44 -0
  56. package/src/styles/components/breadcrumbs.css +13 -0
  57. package/src/styles/components/docs-mobile-nav.css +9 -0
  58. package/src/styles/components/docs-page-nav.css +31 -0
  59. package/src/styles/components/docs-page.css +9 -0
  60. package/src/styles/components/docs-sidebar.css +42 -0
  61. package/src/styles/components/footer.css +8 -0
  62. package/src/styles/components/header.css +20 -0
  63. package/src/styles/components/locale-switcher.css +20 -0
  64. package/src/styles/components/nav-mobile.css +15 -0
  65. package/src/styles/components/nav.css +11 -0
  66. package/src/styles/components/not-found.css +11 -0
  67. package/src/styles/components/page-actions.css +13 -0
  68. package/src/styles/components/scroll-to-top.css +11 -0
  69. package/src/styles/components/search.css +98 -0
  70. package/src/styles/components/shell.css +73 -0
  71. package/src/styles/components/social-links.css +8 -0
  72. package/src/styles/components/theme-switcher.css +14 -0
  73. package/src/styles/components/toc.css +21 -0
  74. package/src/styles/docs.css +112 -0
  75. package/src/styles/global.css +162 -0
  76. package/src/styles/jaamd-forward.css +16 -0
  77. package/src/styles/jaamd-reverse.css +18 -0
  78. package/src/styles/jaamd.css +11 -0
  79. package/src/styles/tailwind.css +30 -0
  80. package/src/themes/index.ts +26 -0
  81. package/src/ui/en.ts +29 -0
  82. package/src/ui/it.ts +30 -0
  83. package/src/ui.ts +49 -0
  84. package/src/urls.ts +49 -0
  85. package/src/utils/docs.ts +300 -0
  86. package/src/utils/endpoints.ts +80 -0
  87. package/src/utils/helpers.ts +4 -0
  88. package/src/utils/search.ts +83 -0
  89. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
+ )}