@jaamd/jaad 0.0.0-stage → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/jaad.css +1331 -0
  4. package/docs/00-overview.md +25 -0
  5. package/docs/01-getting-started/01-installation.md +216 -0
  6. package/docs/01-getting-started/02-pages.md +109 -0
  7. package/docs/01-getting-started/03-deployment.md +100 -0
  8. package/docs/02-configurations/01-reference.md +215 -0
  9. package/docs/02-configurations/02-frontmatter.md +105 -0
  10. package/docs/02-configurations/03-i18n.md +210 -0
  11. package/docs/02-configurations/04-search.md +58 -0
  12. package/docs/02-configurations/05-themes.md +104 -0
  13. package/docs/02-configurations/06-styles.md +156 -0
  14. package/docs/02-configurations/07-custom-pages.md +119 -0
  15. package/docs/02-configurations/08-advanced-layout.md +139 -0
  16. package/docs/03-upgrading.md +208 -0
  17. package/index.ts +31 -0
  18. package/package.json +71 -3
  19. package/site.ts +36 -0
  20. package/src/config.ts +85 -0
  21. package/src/docs/advanced.ts +23 -0
  22. package/src/docs/collection.ts +109 -0
  23. package/src/docs/components/Breadcrumbs.astro +29 -0
  24. package/src/docs/components/DocsMobileNav.astro +90 -0
  25. package/src/docs/components/DocsNavTree.astro +57 -0
  26. package/src/docs/components/DocsPage.astro +66 -0
  27. package/src/docs/components/DocsPageNav.astro +53 -0
  28. package/src/docs/components/DocsSidebarLeft.astro +53 -0
  29. package/src/docs/components/PageActions.astro +83 -0
  30. package/src/docs/components/StructuredData.astro +55 -0
  31. package/src/docs/components/TableOfContents.astro +71 -0
  32. package/src/docs/config.ts +167 -0
  33. package/src/docs/content.ts +36 -0
  34. package/src/docs/endpoints.ts +89 -0
  35. package/src/docs/integration.ts +249 -0
  36. package/src/docs/layouts/DefaultDocsFrame.astro +33 -0
  37. package/src/docs/layouts/Docs.astro +169 -0
  38. package/src/docs/layouts/Page.astro +18 -0
  39. package/src/docs/link-plugins.ts +52 -0
  40. package/src/docs/links.ts +42 -0
  41. package/src/docs/locale-tree.ts +51 -0
  42. package/src/docs/model/ids.ts +87 -0
  43. package/src/docs/model/rendered.ts +185 -0
  44. package/src/docs/model/tree.ts +124 -0
  45. package/src/docs/model/validate.ts +75 -0
  46. package/src/docs/navigation.ts +75 -0
  47. package/src/docs/routes/404.astro +16 -0
  48. package/src/docs/routes/docs-index.astro +19 -0
  49. package/src/docs/routes/docs-slug.astro +54 -0
  50. package/src/docs/routes/docs-slug.md.ts +35 -0
  51. package/src/docs/routes/llms-locale.txt.ts +8 -0
  52. package/src/docs/routes/llms.txt.ts +4 -0
  53. package/src/docs/routes/search-index-locale.json.ts +8 -0
  54. package/src/docs/routes/search-index.json.ts +4 -0
  55. package/src/docs/runtime.ts +17 -0
  56. package/src/docs/search-index.ts +39 -0
  57. package/src/docs/styles/components/breadcrumbs.css +26 -0
  58. package/src/docs/styles/components/docs-mobile-nav.css +47 -0
  59. package/src/docs/styles/components/docs-page-nav.css +41 -0
  60. package/src/docs/styles/components/docs-page.css +9 -0
  61. package/src/docs/styles/components/docs-sidebar.css +59 -0
  62. package/src/docs/styles/components/page-actions.css +51 -0
  63. package/src/docs/styles/components/toc.css +38 -0
  64. package/src/docs/styles/components.css +7 -0
  65. package/src/docs/styles/docs.css +81 -0
  66. package/src/docs/styles/jaamd.css +11 -0
  67. package/src/docs/types.ts +39 -0
  68. package/src/docs/urls.ts +35 -0
  69. package/src/env.d.ts +13 -0
  70. package/src/integration.ts +36 -0
  71. package/src/site/components/Footer.astro +18 -0
  72. package/src/site/components/Head.astro +119 -0
  73. package/src/site/components/Header.astro +50 -0
  74. package/src/site/components/LocaleSwitcher.astro +111 -0
  75. package/src/site/components/Nav.astro +22 -0
  76. package/src/site/components/NavMobile.astro +105 -0
  77. package/src/site/components/SearchPalette.astro +409 -0
  78. package/src/site/components/SocialLinks.astro +27 -0
  79. package/src/site/components/ThemeSwitcher.astro +55 -0
  80. package/src/site/components/locale-switcher.ts +6 -0
  81. package/src/site/config.ts +149 -0
  82. package/src/site/fonts/OFL.txt +94 -0
  83. package/src/site/fonts/inter.woff2 +0 -0
  84. package/src/site/fonts/merriweather-italic.woff2 +0 -0
  85. package/src/site/fonts/merriweather.woff2 +0 -0
  86. package/src/site/fonts.ts +29 -0
  87. package/src/site/forges.ts +60 -0
  88. package/src/site/helpers.ts +4 -0
  89. package/src/site/infer.ts +75 -0
  90. package/src/site/integration.ts +59 -0
  91. package/src/site/layouts/Shell.astro +41 -0
  92. package/src/site/locales.ts +199 -0
  93. package/src/site/runtime.ts +19 -0
  94. package/src/site/search.ts +227 -0
  95. package/src/site/social.ts +44 -0
  96. package/src/site/styles/components/footer.css +8 -0
  97. package/src/site/styles/components/header.css +31 -0
  98. package/src/site/styles/components/locale-switcher.css +20 -0
  99. package/src/site/styles/components/nav-mobile.css +15 -0
  100. package/src/site/styles/components/nav.css +11 -0
  101. package/src/site/styles/components/not-found.css +11 -0
  102. package/src/site/styles/components/search.css +118 -0
  103. package/src/site/styles/components/shell.css +77 -0
  104. package/src/site/styles/components/social-links.css +8 -0
  105. package/src/site/styles/components/theme-switcher.css +14 -0
  106. package/src/site/styles/components.css +10 -0
  107. package/src/site/styles/global.css +140 -0
  108. package/src/site/styles/jaamd-forward.css +16 -0
  109. package/src/site/styles/jaamd-reverse.css +18 -0
  110. package/src/site/styles/tailwind.css +30 -0
  111. package/src/site/themes.ts +26 -0
  112. package/src/site/ui/en.ts +27 -0
  113. package/src/site/ui/it.ts +28 -0
  114. package/src/site/ui.ts +49 -0
  115. package/src/site/urls.ts +14 -0
  116. package/src/site/virtual.ts +42 -0
  117. package/src/styles.css +5 -0
@@ -0,0 +1,51 @@
1
+ @layer components {
2
+ .docs-page-actions {
3
+ display: flex;
4
+ flex-wrap: wrap;
5
+ gap: 0.25rem 1.25rem;
6
+ margin-top: 2rem;
7
+ font-size: 0.8125rem;
8
+ }
9
+
10
+ .docs-page-action {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 0.5rem;
14
+ min-height: 2.75rem;
15
+ padding: 0;
16
+ border: 0;
17
+ background: none;
18
+ color: var(--color-foreground-muted);
19
+ font: inherit;
20
+ text-decoration: none;
21
+ cursor: pointer;
22
+ }
23
+
24
+ .docs-page-action:hover {
25
+ color: var(--color-foreground-bright);
26
+ }
27
+
28
+ .docs-page-action-icon {
29
+ flex: none;
30
+ width: 0.875rem;
31
+ height: 0.875rem;
32
+ }
33
+
34
+ /* Under the table of contents: a column of quiet links. */
35
+ @media (min-width: 64rem) {
36
+ .docs-page-actions {
37
+ flex-direction: column;
38
+ margin-top: 0;
39
+ }
40
+
41
+ .docs-page-action {
42
+ min-height: 1.75rem;
43
+ }
44
+
45
+ .docs-toc ~ .docs-page-actions {
46
+ margin-top: 1.5rem;
47
+ padding-top: 1rem;
48
+ border-top: 1px solid var(--color-border);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,38 @@
1
+ @layer components {
2
+ .docs-toc-title {
3
+ margin: 0 0 0.5rem;
4
+ font-family: var(--font-sans);
5
+ font-size: 0.75rem;
6
+ font-weight: 500;
7
+ color: var(--color-foreground-muted);
8
+ }
9
+
10
+ .docs-toc-list {
11
+ margin: 0;
12
+ padding: 0;
13
+ list-style: none;
14
+ }
15
+
16
+ .docs-toc-link {
17
+ display: block;
18
+ padding: 0.1875rem 0 0.1875rem calc(0.625rem - 2px);
19
+ border-left: 2px solid transparent;
20
+ color: var(--color-foreground-muted);
21
+ font-size: 0.8125rem;
22
+ line-height: 1.5;
23
+ text-decoration: none;
24
+ }
25
+
26
+ .docs-toc-link-nested {
27
+ padding-left: calc(1.375rem - 2px);
28
+ }
29
+
30
+ .docs-toc-link:hover {
31
+ color: var(--color-foreground-bright);
32
+ }
33
+
34
+ .docs-toc-link[aria-current='location'] {
35
+ border-color: var(--color-foreground-bright);
36
+ color: var(--color-foreground-bright);
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ @import './components/docs-page.css';
2
+ @import './components/docs-sidebar.css';
3
+ @import './components/toc.css';
4
+ @import './components/docs-mobile-nav.css';
5
+ @import './components/docs-page-nav.css';
6
+ @import './components/page-actions.css';
7
+ @import './components/breadcrumbs.css';
@@ -0,0 +1,81 @@
1
+ /* Layered, so a page's own utilities still win. */
2
+ @layer components {
3
+ .docs-frame {
4
+ display: contents;
5
+ }
6
+
7
+ /* The header's column. Wide, it holds the page between two sidebars fixed
8
+ to the window: only the page scrolls. */
9
+ .docs-container {
10
+ width: 100%;
11
+ max-width: var(--jaad-chrome-width);
12
+ margin-inline: auto;
13
+ padding-inline: 1rem;
14
+ }
15
+
16
+ .docs-main {
17
+ width: 100%;
18
+ min-width: 0;
19
+ max-width: var(--jaad-content-width);
20
+ margin-inline: auto;
21
+ }
22
+
23
+ .docs-page-main {
24
+ padding-top: 2rem;
25
+ }
26
+
27
+ [data-jaad-default-frame] .docs-container > .jaad-footer {
28
+ width: 100%;
29
+ max-width: var(--jaad-content-width);
30
+ margin-inline: auto;
31
+ }
32
+
33
+ @media (48rem <= width < 64rem) {
34
+ .docs-page-main {
35
+ padding-inline: var(--jaad-page-padding);
36
+ }
37
+ }
38
+
39
+ @media (min-width: 64rem) {
40
+ .docs-page-main {
41
+ padding-top: 2.5rem;
42
+ }
43
+
44
+ [data-jaad-default-frame] .docs-container {
45
+ display: grid;
46
+ grid-template-columns: var(--jaad-sidebar-width) minmax(0, 1fr) var(--jaad-sidebar-width);
47
+ column-gap: 3rem;
48
+ }
49
+
50
+ [data-jaad-default-frame] .docs-main,
51
+ [data-jaad-default-frame] .docs-container > .jaad-footer {
52
+ grid-column: 2;
53
+ }
54
+
55
+ [data-jaad-default-frame] .docs-sidebar-left,
56
+ [data-jaad-default-frame] .docs-sidebar-right {
57
+ position: fixed;
58
+ top: var(--jaad-header-height);
59
+ bottom: 0;
60
+ padding-block: 2rem;
61
+ }
62
+
63
+ /* Widened into the gutter, where the bar of the current page sits. */
64
+ [data-jaad-default-frame] .docs-sidebar-left {
65
+ left: max(1rem, calc(50% - var(--jaad-chrome-width) / 2 + 1rem));
66
+ margin-left: -0.75rem;
67
+ padding-left: 0.75rem;
68
+ }
69
+
70
+ [data-jaad-default-frame] .docs-sidebar-right {
71
+ right: max(1rem, calc(50% - var(--jaad-chrome-width) / 2 + 1rem));
72
+ }
73
+ }
74
+
75
+ @media (width < 64rem) {
76
+ [data-jaad-default-frame] .docs-sidebar-left,
77
+ [data-jaad-default-frame] .docs-sidebar-right .docs-toc {
78
+ display: none;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,11 @@
1
+ /* Bridge to JAAMD. */
2
+
3
+ :root {
4
+ --jaamd-font-sans: var(--font-sans);
5
+ --jaamd-font-serif: var(--font-serif);
6
+ --jaamd-font-mono: var(--font-mono);
7
+ --jaamd-font-size: 1.125rem;
8
+ --jaamd-line-height: 1.6;
9
+
10
+ --jaamd-scroll-margin-top: 0;
11
+ }
@@ -0,0 +1,39 @@
1
+ import type { RenderedMarkdown } from './model/rendered.ts'
2
+
3
+ export interface DocsNavItem {
4
+ title: string
5
+ /** Set when the docs are split into sections. */
6
+ section?: string
7
+ chapter?: string
8
+ href: string
9
+ isActive: boolean
10
+ }
11
+
12
+ export interface DocsHeading {
13
+ depth: 2 | 3
14
+ slug: string
15
+ text: string
16
+ }
17
+
18
+ /** Resolved frontmatter. Mirrors the schema in content.ts. */
19
+ export interface DocsPageData {
20
+ title?: string
21
+ description?: string
22
+ label?: string
23
+ translationKey?: string
24
+ order?: number
25
+ draft: boolean
26
+ keywords: string[]
27
+ author?: string
28
+ ogImage?: string | false
29
+ lastUpdated?: Date
30
+ }
31
+
32
+ /** The shape jaad requires of a docs collection entry. Astro generates
33
+ * collection types in the consumer project, so the package states its own contract. */
34
+ export interface DocsEntry {
35
+ id: string
36
+ body?: string
37
+ data: DocsPageData
38
+ rendered?: RenderedMarkdown
39
+ }
@@ -0,0 +1,35 @@
1
+ import { localeSegment } from '../site/locales.ts'
2
+ import { normaliseBasePath, withDeploymentBase } from '../site/urls.ts'
3
+
4
+ export interface DocsUrlOptions {
5
+ /** routeBase, normalised here: empty at the application root. */
6
+ docsBase: string
7
+ /** Astro's deployment base. */
8
+ deploymentBase?: string
9
+ /** Locale of the page, when the docs are localised. */
10
+ locale?: string
11
+ /** The one locale that owns the unprefixed urls. */
12
+ defaultLocale?: string
13
+ }
14
+
15
+ /** Public href for a documentation page. Empty slug means the opening page. */
16
+ export function docsPageHref(slug: string, urls: DocsUrlOptions): string {
17
+ return docsHref(slug, urls)
18
+ }
19
+
20
+ /** Raw Markdown keeps the page's named slug, including for the opener. */
21
+ export function docsMarkdownHref(slug: string, urls: DocsUrlOptions): string {
22
+ return docsHref(slug, urls, '.md')
23
+ }
24
+
25
+ function docsHref(
26
+ slug: string,
27
+ { docsBase, deploymentBase = '', locale, defaultLocale }: DocsUrlOptions,
28
+ suffix = '',
29
+ ): string {
30
+ const mount = normaliseBasePath(docsBase)
31
+ const path = [localeSegment(locale, defaultLocale), slug].filter(Boolean).join('/')
32
+ // Without a suffix an empty path is the mount itself; with one it is a file.
33
+ const href = path || suffix ? `${mount}/${path}${suffix}` : mount || '/'
34
+ return withDeploymentBase(href, deploymentBase)
35
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /// <reference types="astro/client" />
2
+
3
+ declare module 'virtual:jaad/config' {
4
+ // oxlint-disable-next-line typescript/consistent-type-imports
5
+ const config: import('./config.ts').JaadResolvedConfig
6
+ export default config
7
+ }
8
+
9
+ declare module 'virtual:jaad/docs-frame' {
10
+ // oxlint-disable-next-line typescript/consistent-type-imports
11
+ const DocsFrame: typeof import('./docs/layouts/DefaultDocsFrame.astro').default
12
+ export default DocsFrame
13
+ }
@@ -0,0 +1,36 @@
1
+ import type { JaadResolvedConfig } from './config.ts'
2
+ import type { IntegrationState } from './docs/integration.ts'
3
+ import type { SiteStylesheets } from './site/virtual.ts'
4
+ import type { AstroIntegration } from 'astro'
5
+ import { setupDocs } from './docs/integration.ts'
6
+ import { setupSite } from './site/integration.ts'
7
+ import { normaliseBasePath } from './site/urls.ts'
8
+
9
+ /** The site, then the docs on it. */
10
+ export function createJaadIntegration(
11
+ config: JaadResolvedConfig,
12
+ stylesheets: SiteStylesheets,
13
+ state: IntegrationState,
14
+ discoverAliases: boolean,
15
+ ): AstroIntegration {
16
+ return {
17
+ name: 'jaad',
18
+ hooks: {
19
+ 'astro:config:setup': (params) => {
20
+ setupSite(params, config, stylesheets)
21
+ setupDocs(params, config, state, discoverAliases)
22
+ },
23
+
24
+ 'astro:config:done': ({ config: astroConfig, injectTypes }) => {
25
+ state.deploymentBase = normaliseBasePath(astroConfig.base)
26
+ injectTypes({
27
+ filename: 'jaad.d.ts',
28
+ content: `declare module "virtual:jaad/config" {
29
+ const config: import("@jaamd/jaad").JaadResolvedConfig;
30
+ export default config;
31
+ }`,
32
+ })
33
+ },
34
+ },
35
+ }
36
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ import config from 'virtual:jaad/config'
3
+
4
+ const CREDIT = 'Built with <a href="https://docs.jaamd.org/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,50 @@
1
+ ---
2
+ import type { LocaleAlternate } from '../locales.ts'
3
+ import config from 'virtual:jaad/config'
4
+ import { DEFAULT_LOCALE, ui } from '../runtime.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
+ locale?: string
15
+ /** The page in every locale, for the switcher. */
16
+ alternates: LocaleAlternate[]
17
+ search: {
18
+ /** `search-index.json` of the reader's locale. */
19
+ indexUrl: string
20
+ /** The full search field on wide screens, not just its icon. */
21
+ prominent?: boolean
22
+ }
23
+ }
24
+
25
+ const { locale = DEFAULT_LOCALE, alternates, search } = Astro.props
26
+ const t = ui(locale)
27
+ const homeHref = withDeploymentBase(config.homeLink, import.meta.env.BASE_URL)
28
+ const logoSrc = config.logo ? withDeploymentBase(config.logo, import.meta.env.BASE_URL) : null
29
+ ---
30
+
31
+ <header class="jaad-header" transition:persist>
32
+ <div class="jaad-header-inner">
33
+ <div class="jaad-header-row">
34
+ <span class="jaad-brand font-brand">
35
+ <a href={homeHref} title={t('home')} class="jaad-brand-link">
36
+ {logoSrc ? <img src={logoSrc} alt={config.title} class="jaad-logo" /> : config.title}
37
+ </a>
38
+ </span>
39
+
40
+ <div class="jaad-header-actions">
41
+ <SearchPalette indexUrl={search.indexUrl} prominent={search.prominent} {locale} />
42
+ <Nav />
43
+ <SocialLinks />
44
+ {config.appearance === 'auto' && <ThemeSwitcher {locale} />}
45
+ {alternates.length > 1 && <LocaleSwitcher {alternates} current={locale} />}
46
+ <NavMobile {locale} />
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </header>
@@ -0,0 +1,111 @@
1
+ ---
2
+ import type { LocaleAlternate } from '../locales.ts'
3
+ import { ui } from '../runtime.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
+ )}