@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,124 @@
1
+ import type { DocsPageData } from '../types.ts'
2
+ import type { DocsUrlOptions } from '../urls.ts'
3
+ import type { RenderedMarkdown } from './rendered.ts'
4
+ import { docsPageHref } from '../urls.ts'
5
+ import { namedSlug, parseDocPath, slugToTitle } from './ids.ts'
6
+ import { renderedTitle } from './rendered.ts'
7
+
8
+ /** A markdown file of one locale: a collection entry, or a file read from disk. */
9
+ export interface DocsSource {
10
+ /** Collection id. */
11
+ id: string
12
+ /** The id without its locale and its section. */
13
+ path: string
14
+ /** Set when the docs are localised. */
15
+ locale?: string
16
+ /** Set when the docs are split into sections. */
17
+ section?: string
18
+ data: Partial<DocsPageData>
19
+ rendered?: RenderedMarkdown
20
+ }
21
+
22
+ /** A page with everything its file, its frontmatter and its place in the docs decide. */
23
+ export interface DocsPage<S extends DocsSource = DocsSource> {
24
+ /** What the page was built from. */
25
+ source: S
26
+ id: string
27
+ path: string
28
+ locale?: string
29
+ section?: string
30
+ /** The chapter directory, without its order prefix. */
31
+ chapter?: string
32
+ data: S['data']
33
+ /** The named url below the docs mount: its section, then its clean path. */
34
+ slug: string
35
+ /** Where it is served: empty for the opening page, the section for the first
36
+ * page of a later one, `slug` otherwise. */
37
+ canonical: string
38
+ /** The other slugs it answers to, each redirecting to `canonical`. */
39
+ aliases: string[]
40
+ /** The url of `canonical`. */
41
+ href: string
42
+ /** Frontmatter title, then the first h1, then the file name. */
43
+ title: string
44
+ /** The name in navigation: frontmatter `label`, then `title`. */
45
+ label: string
46
+ /** What pairs it with its translations: frontmatter `translationKey`, then `slug`. */
47
+ translationKey: string
48
+ /** Position in reading order. */
49
+ index: number
50
+ }
51
+
52
+ /** The pages of one locale in reading order. */
53
+ export interface DocsTree<S extends DocsSource = DocsSource> {
54
+ pages: DocsPage<S>[]
55
+ byId: Map<string, DocsPage<S>>
56
+ byKey: Map<string, DocsPage<S>>
57
+ }
58
+
59
+ export interface DocsTreeOptions {
60
+ /** Section keys, in their order. */
61
+ sections?: string[]
62
+ /** For `href`. */
63
+ urls: DocsUrlOptions
64
+ }
65
+
66
+ /** The order of `sections` first, then chapter order, then file order within
67
+ * the chapter. Frontmatter `order` replaces a file's prefix number; root pages
68
+ * interleave with chapters by their own number. */
69
+ function sortSources<S extends DocsSource>(sources: S[], sections: string[]): S[] {
70
+ return sources
71
+ .map((source) => {
72
+ const parsed = parseDocPath(source.path)
73
+ const order = source.data.order ?? parsed.order
74
+ return {
75
+ source,
76
+ section: sections.indexOf(source.section ?? ''),
77
+ primary: parsed.orderChapter ?? order,
78
+ secondary: order,
79
+ }
80
+ })
81
+ .toSorted((a, b) => a.section - b.section || a.primary - b.primary || a.secondary - b.secondary)
82
+ .map((entry) => entry.source)
83
+ }
84
+
85
+ /** Resolves every page of one locale once. */
86
+ export function buildDocsTree<S extends DocsSource>(sources: S[], options: DocsTreeOptions): DocsTree<S> {
87
+ const sorted = sortSources(sources, options.sections ?? [])
88
+
89
+ const pages = sorted.map((source, index): DocsPage<S> => {
90
+ const parsed = parseDocPath(source.path)
91
+ const slug = namedSlug(source.path, source.section)
92
+ const canonical =
93
+ index === 0 ? '' : source.section && sorted[index - 1].section !== source.section ? source.section : slug
94
+ const title = source.data.title ?? renderedTitle(source.rendered) ?? slugToTitle(parsed.slug)
95
+
96
+ return {
97
+ source,
98
+ id: source.id,
99
+ path: source.path,
100
+ locale: source.locale,
101
+ section: source.section,
102
+ chapter: parsed.chapter,
103
+ data: source.data,
104
+ slug,
105
+ canonical,
106
+ aliases: [
107
+ ...(slug === canonical ? [] : [slug]),
108
+ // The first section's key leads to the opening page.
109
+ ...(index === 0 && source.section ? [source.section] : []),
110
+ ],
111
+ href: docsPageHref(canonical, options.urls),
112
+ title,
113
+ label: source.data.label ?? title,
114
+ translationKey: source.data.translationKey ?? slug,
115
+ index,
116
+ }
117
+ })
118
+
119
+ return {
120
+ pages,
121
+ byId: new Map(pages.map((page) => [page.id, page])),
122
+ byKey: new Map(pages.map((page) => [page.translationKey, page])),
123
+ }
124
+ }
@@ -0,0 +1,75 @@
1
+ import type { DocsSource } from './tree.ts'
2
+ import { getCleanSlug, namedSlug } from './ids.ts'
3
+
4
+ /** Within a section, like `id`; across them, the section is part of the name. */
5
+ const inSection = (page: DocsSource, id: string) => (page.section ? `${page.section}/${id}` : id)
6
+
7
+ /** Fail before route generation when the file tree cannot produce one clear
8
+ * navigation and URL for every page. */
9
+ export function validateDocsStructure(pages: DocsSource[], locale?: string): void {
10
+ const idsBySlug = new Map<string, string[]>()
11
+ const directoriesByChapter = new Map<string, Set<string>>()
12
+ const issues: string[] = []
13
+ // Messages name the file on disk, locale directory included.
14
+ const named = (id: string) => (locale ? `${locale}/${id}` : id)
15
+
16
+ const byId = pages.map((page) => ({ page, id: inSection(page, page.path) }))
17
+ for (const { page, id } of byId.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
18
+ const parts = page.path.split('/')
19
+ if (parts.length > 2) {
20
+ issues.push(`${named(id)}: only one chapter directory is supported inside docs/`)
21
+ }
22
+
23
+ const slug = namedSlug(page.path, page.section)
24
+ const ids = idsBySlug.get(slug) ?? []
25
+ ids.push(id)
26
+ idsBySlug.set(slug, ids)
27
+
28
+ if (parts.length === 2) {
29
+ const directory = inSection(page, parts[0])
30
+ const chapter = inSection(page, getCleanSlug(parts[0]))
31
+ const directories = directoriesByChapter.get(chapter) ?? new Set()
32
+ directories.add(directory)
33
+ directoriesByChapter.set(chapter, directories)
34
+ }
35
+ }
36
+
37
+ for (const [slug, ids] of idsBySlug) {
38
+ if (ids.length > 1) {
39
+ issues.push(`${ids.map(named).join(', ')} all resolve to /${slug}`)
40
+ }
41
+ }
42
+
43
+ // A page outside the sections cannot take a section's url.
44
+ const keys = new Set(pages.flatMap((page) => page.section ?? []))
45
+ for (const page of pages) {
46
+ const first = namedSlug(page.path, page.section).split('/')[0]
47
+ if (!page.section && keys.has(first)) {
48
+ issues.push(`${named(page.path)} resolves into section /${first}`)
49
+ }
50
+ }
51
+
52
+ const byKey = new Map<string, string[]>()
53
+ for (const page of pages) {
54
+ const key = page.data.translationKey
55
+ if (!key) continue
56
+ const ids = byKey.get(key) ?? []
57
+ ids.push(inSection(page, page.path))
58
+ byKey.set(key, ids)
59
+ }
60
+ for (const [key, ids] of byKey) {
61
+ if (ids.length > 1) {
62
+ issues.push(`${ids.map(named).join(', ')} all claim translationKey ${key}`)
63
+ }
64
+ }
65
+
66
+ for (const [chapter, directories] of directoriesByChapter) {
67
+ if (directories.size > 1) {
68
+ issues.push(`${[...directories].map(named).join(', ')} all resolve to chapter /${chapter}`)
69
+ }
70
+ }
71
+
72
+ if (issues.length > 0) {
73
+ throw new Error(`jaad: invalid documentation structure\n${issues.map((issue) => ` ${issue}`).join('\n')}`)
74
+ }
75
+ }
@@ -0,0 +1,75 @@
1
+ import type { DocsTree } from './model/tree.ts'
2
+ import type { DocsNavItem } from './types.ts'
3
+
4
+ /** A page, or a chapter and its pages. */
5
+ export type DocsNavEntry =
6
+ | { type: 'page'; item: DocsNavItem }
7
+ | { type: 'chapter'; chapter: string; items: DocsNavItem[] }
8
+
9
+ /** A top-level entry. With sections, the pages outside them come first, then
10
+ * every section and its entries. */
11
+ export type DocsNavGroup = DocsNavEntry | { type: 'section'; section: string; label: string; entries: DocsNavEntry[] }
12
+
13
+ export interface DocsNavigation {
14
+ /** The whole tree, in reading order. */
15
+ groups: DocsNavGroup[]
16
+ previousPage: DocsNavItem | null
17
+ nextPage: DocsNavItem | null
18
+ }
19
+
20
+ /** Each chapter sits where its first page does. */
21
+ function groupByChapter(items: DocsNavItem[]): DocsNavEntry[] {
22
+ const chapters = new Map<string, DocsNavItem[]>()
23
+ const groups: DocsNavEntry[] = []
24
+
25
+ for (const item of items) {
26
+ if (!item.chapter) {
27
+ groups.push({ type: 'page', item })
28
+ continue
29
+ }
30
+
31
+ let pages = chapters.get(item.chapter)
32
+ if (!pages) {
33
+ pages = []
34
+ chapters.set(item.chapter, pages)
35
+ groups.push({ type: 'chapter', chapter: item.chapter, items: pages })
36
+ }
37
+ pages.push(item)
38
+ }
39
+
40
+ return groups
41
+ }
42
+
43
+ /** Each section with pages, under its label, in the order of `sections`. */
44
+ function groupBySection(items: DocsNavItem[], sections: { key: string; label: string }[]): DocsNavGroup[] {
45
+ return sections.flatMap(({ key, label }) => {
46
+ const own = items.filter((item) => item.section === key)
47
+ return own.length > 0 ? [{ type: 'section' as const, section: key, label, entries: groupByChapter(own) }] : []
48
+ })
49
+ }
50
+
51
+ /** The navigation seen from one page of `tree`: every group, the page marked
52
+ * active, and its neighbours. */
53
+ export function buildDocsNavigation(
54
+ tree: DocsTree,
55
+ currentId: string,
56
+ sections: { key: string; label: string }[] = [],
57
+ ): DocsNavigation {
58
+ const position = tree.byId.get(currentId)?.index ?? -1
59
+ const items = tree.pages.map((page): DocsNavItem => ({
60
+ title: page.label,
61
+ section: page.section,
62
+ chapter: page.chapter,
63
+ href: page.href,
64
+ isActive: page.index === position,
65
+ }))
66
+
67
+ return {
68
+ groups:
69
+ sections.length > 0
70
+ ? [...groupByChapter(items.filter((item) => !item.section)), ...groupBySection(items, sections)]
71
+ : groupByChapter(items),
72
+ previousPage: position > 0 ? items[position - 1] : null,
73
+ nextPage: position >= 0 && position < items.length - 1 ? items[position + 1] : null,
74
+ }
75
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { withDeploymentBase } from '../../site/urls.ts'
3
+ import Layout from '../layouts/Page.astro'
4
+
5
+ const homeHref = withDeploymentBase('/', import.meta.env.BASE_URL)
6
+ ---
7
+
8
+ <Layout title="404" description="The page you're looking for doesn't exist.">
9
+ <div class="jaad-not-found">
10
+ <h1 class="jaad-not-found-title">404</h1>
11
+ <p class="jaad-not-found-text">
12
+ <i>Oops!</i> The page you're looking for doesn't exist.
13
+ </p>
14
+ <a href={homeHref}>Go back to the homepage</a>
15
+ </div>
16
+ </Layout>
@@ -0,0 +1,19 @@
1
+ ---
2
+ import { render } from 'astro:content'
3
+ import { withDeploymentBase } from '../../site/urls.ts'
4
+ import { getDocsTree } from '../collection.ts'
5
+ import DocsPage from '../components/DocsPage.astro'
6
+
7
+ const tree = await getDocsTree()
8
+
9
+ const page = tree.pages[0]
10
+ if (!page) {
11
+ return Astro.redirect(withDeploymentBase('/404', import.meta.env.BASE_URL))
12
+ }
13
+
14
+ const { Content } = await render(page.source)
15
+ ---
16
+
17
+ <DocsPage {page} {tree}>
18
+ <Content />
19
+ </DocsPage>
@@ -0,0 +1,54 @@
1
+ ---
2
+ import { render } from 'astro:content'
3
+ import { buildLocales, localePrefix } from '../../site/runtime.ts'
4
+ import { withDeploymentBase } from '../../site/urls.ts'
5
+ import { getDocsTree } from '../collection.ts'
6
+ import DocsPage from '../components/DocsPage.astro'
7
+ import { docsUrls } from '../runtime.ts'
8
+ import { docsPageHref } from '../urls.ts'
9
+
10
+ interface DocsPath {
11
+ params: { slug: string }
12
+ /** `redirect` is the canonical slug of a page reached by another name. */
13
+ props: { id: string; locale?: string; redirect?: string }
14
+ }
15
+
16
+ export async function getStaticPaths() {
17
+ const paths: DocsPath[] = []
18
+
19
+ for (const locale of buildLocales()) {
20
+ const { pages } = await getDocsTree(locale)
21
+ const prefix = localePrefix(locale)
22
+
23
+ for (const page of pages) {
24
+ const add = (slug: string, redirect?: string) =>
25
+ paths.push({ params: { slug }, props: { id: page.id, locale, redirect } })
26
+
27
+ // Empty is the index route's; a prefixed locale has none, so /it is this page.
28
+ const own = `${prefix}${page.canonical}`.replace(/\/$/, '')
29
+ if (own) add(own)
30
+ for (const alias of page.aliases) add(`${prefix}${alias}`, page.canonical)
31
+ }
32
+ }
33
+
34
+ return paths
35
+ }
36
+
37
+ const { id, locale, redirect } = Astro.props
38
+
39
+ if (redirect !== undefined) {
40
+ return Astro.redirect(docsPageHref(redirect, docsUrls(locale)), 301)
41
+ }
42
+
43
+ const tree = await getDocsTree(locale)
44
+ const page = tree.byId.get(id)
45
+ if (!page) {
46
+ return Astro.redirect(withDeploymentBase('/404', import.meta.env.BASE_URL))
47
+ }
48
+
49
+ const { Content } = await render(page.source)
50
+ ---
51
+
52
+ <DocsPage {page} {tree} headTitle={page.title}>
53
+ <Content />
54
+ </DocsPage>
@@ -0,0 +1,35 @@
1
+ import type { APIRoute, GetStaticPaths } from 'astro'
2
+ import { buildLocales, localePrefix } from '../../site/runtime.ts'
3
+ import { getDocsTree } from '../collection.ts'
4
+
5
+ /** Raw markdown at the page's own url plus `.md`, for readers and for LLMs. */
6
+ export const getStaticPaths: GetStaticPaths = async () => {
7
+ const paths: {
8
+ params: { slug: string }
9
+ props: { id: string; locale?: string }
10
+ }[] = []
11
+
12
+ for (const locale of buildLocales()) {
13
+ const { pages } = await getDocsTree(locale)
14
+ const prefix = localePrefix(locale)
15
+
16
+ for (const page of pages) {
17
+ paths.push({
18
+ params: { slug: `${prefix}${page.slug}` },
19
+ props: { id: page.id, locale },
20
+ })
21
+ }
22
+ }
23
+
24
+ return paths
25
+ }
26
+
27
+ export const GET: APIRoute = async ({ props }) => {
28
+ const { id, locale } = props as { id: string; locale?: string }
29
+ const page = (await getDocsTree(locale)).byId.get(id)
30
+ if (!page) return new Response('Not found', { status: 404 })
31
+
32
+ return new Response(page.source.body ?? '', {
33
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
34
+ })
35
+ }
@@ -0,0 +1,8 @@
1
+ import type { APIRoute, GetStaticPaths } from 'astro'
2
+ import { getServedLocales } from '../collection.ts'
3
+ import { llmsResponse } from '../endpoints.ts'
4
+
5
+ export const getStaticPaths: GetStaticPaths = async () =>
6
+ (await getServedLocales()).map((locale) => ({ params: { locale } }))
7
+
8
+ export const GET: APIRoute = ({ site, params }) => llmsResponse(site, params.locale)
@@ -0,0 +1,4 @@
1
+ import type { APIRoute } from 'astro'
2
+ import { llmsResponse } from '../endpoints.ts'
3
+
4
+ export const GET: APIRoute = ({ site }) => llmsResponse(site)
@@ -0,0 +1,8 @@
1
+ import type { APIRoute, GetStaticPaths } from 'astro'
2
+ import { getServedLocales } from '../collection.ts'
3
+ import { searchIndexResponse } from '../endpoints.ts'
4
+
5
+ export const getStaticPaths: GetStaticPaths = async () =>
6
+ (await getServedLocales()).map((locale) => ({ params: { locale } }))
7
+
8
+ export const GET: APIRoute = ({ params }) => searchIndexResponse(params.locale)
@@ -0,0 +1,4 @@
1
+ import type { APIRoute } from 'astro'
2
+ import { searchIndexResponse } from '../endpoints.ts'
3
+
4
+ export const GET: APIRoute = () => searchIndexResponse()
@@ -0,0 +1,17 @@
1
+ import type { DocsSection } from './config.ts'
2
+ import type { DocsUrlOptions } from './urls.ts'
3
+ import config from 'virtual:jaad/config'
4
+ import { DEFAULT_LOCALE } from '../site/runtime.ts'
5
+
6
+ /** The virtual config, read once. */
7
+ export const SECTIONS: DocsSection[] = config.docs.sections
8
+
9
+ /** The four fields every href builder takes, in one place. */
10
+ export function docsUrls(locale?: string): DocsUrlOptions {
11
+ return {
12
+ docsBase: config.docs.base,
13
+ deploymentBase: import.meta.env.BASE_URL,
14
+ locale,
15
+ defaultLocale: DEFAULT_LOCALE,
16
+ }
17
+ }
@@ -0,0 +1,39 @@
1
+ import type { SearchIndex } from '../site/search.ts'
2
+ import type { DocsSection } from './config.ts'
3
+ import type { DocsTree } from './model/tree.ts'
4
+ import { formatChapterTitle } from './model/ids.ts'
5
+ import { pageChunks } from './model/rendered.ts'
6
+
7
+ /** One entry per h2 and h3 of every page, plus one for the text before them. */
8
+ export function buildSearchIndex(tree: DocsTree, sections: Pick<DocsSection, 'key' | 'label'>[]): SearchIndex {
9
+ const index: SearchIndex = {
10
+ sections: sections
11
+ .filter(({ key }) => tree.pages.some((page) => page.section === key))
12
+ .map(({ key, label }) => ({ key, label })),
13
+ pages: [],
14
+ entries: [],
15
+ }
16
+
17
+ for (const page of tree.pages) {
18
+ const chapter = formatChapterTitle(page.chapter)
19
+ const keywords = page.data.keywords ?? []
20
+ index.pages.push({
21
+ title: page.title,
22
+ href: page.href,
23
+ ...(page.section ? { section: page.section } : {}),
24
+ ...(chapter ? { chapter } : {}),
25
+ ...(keywords.length > 0 ? { keywords } : {}),
26
+ })
27
+
28
+ for (const { heading, text } of pageChunks(page.source.rendered)) {
29
+ if (!heading && !text) continue
30
+ index.entries.push({
31
+ page: page.index,
32
+ ...(heading ? { heading: heading.text, anchor: heading.slug } : {}),
33
+ text,
34
+ })
35
+ }
36
+ }
37
+
38
+ return index
39
+ }
@@ -0,0 +1,26 @@
1
+ @layer components {
2
+ .docs-breadcrumbs {
3
+ display: flex;
4
+ flex-wrap: wrap;
5
+ gap: 0.375rem;
6
+ margin-bottom: 0.5rem;
7
+ font-size: 0.8125rem;
8
+ color: var(--color-foreground-muted);
9
+ }
10
+
11
+ .docs-breadcrumbs-link {
12
+ color: inherit;
13
+ text-decoration: none;
14
+ }
15
+
16
+ .docs-breadcrumbs-link:hover {
17
+ color: var(--color-foreground-bright);
18
+ }
19
+
20
+ /* Narrow, the navigation menu above already names them. */
21
+ @media (width < 64rem) {
22
+ [data-jaad-default-frame] .docs-breadcrumbs {
23
+ display: none;
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,47 @@
1
+ @layer components {
2
+ .docs-mobile-nav {
3
+ margin-bottom: 1.5rem;
4
+ border-top: 1px solid var(--color-border);
5
+ font-size: 0.875rem;
6
+ }
7
+
8
+ .docs-mobile-menu {
9
+ border-bottom: 1px solid var(--color-border);
10
+ }
11
+
12
+ .docs-mobile-summary {
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ align-items: center;
16
+ gap: 0.375rem;
17
+ min-height: 2.75rem;
18
+ color: var(--color-foreground-muted);
19
+ cursor: pointer;
20
+ list-style: none;
21
+ }
22
+
23
+ .docs-mobile-summary::-webkit-details-marker {
24
+ display: none;
25
+ }
26
+
27
+ .docs-mobile-icon {
28
+ width: 1rem;
29
+ height: 1rem;
30
+ margin-right: 0.25rem;
31
+ }
32
+
33
+ .docs-mobile-current {
34
+ color: var(--color-foreground-bright);
35
+ font-weight: 500;
36
+ }
37
+
38
+ .docs-mobile-tree {
39
+ padding: 0.25rem 0 1rem 0.75rem;
40
+ }
41
+
42
+ @media (min-width: 64rem) {
43
+ .docs-mobile-nav {
44
+ display: none;
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,41 @@
1
+ @layer components {
2
+ .docs-page-nav {
3
+ margin-top: 3rem;
4
+ padding-top: 0.5rem;
5
+ border-top: 1px solid var(--color-border);
6
+ }
7
+
8
+ .docs-page-nav-row {
9
+ display: flex;
10
+ justify-content: space-between;
11
+ gap: 1rem;
12
+ }
13
+
14
+ .docs-page-nav-link {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 0.5rem;
18
+ min-width: 0;
19
+ max-width: calc(50% - 0.5rem);
20
+ min-height: 2.75rem;
21
+ color: var(--color-foreground-bright);
22
+ font-size: 0.875rem;
23
+ text-decoration: none;
24
+ }
25
+
26
+ .docs-page-nav-link:hover {
27
+ color: var(--color-foreground-muted);
28
+ }
29
+
30
+ .docs-page-nav-icon {
31
+ flex: none;
32
+ width: 1rem;
33
+ height: 1rem;
34
+ }
35
+
36
+ .docs-page-nav-label {
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ white-space: nowrap;
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ @layer components {
2
+ .docs-content {
3
+ @apply w-full leading-relaxed wrap-break-word;
4
+ }
5
+
6
+ .docs-updated {
7
+ @apply text-foreground-muted mt-8 text-xs;
8
+ }
9
+ }
@@ -0,0 +1,59 @@
1
+ @layer components {
2
+ .docs-sidebar-left nav {
3
+ font-size: 0.875rem;
4
+ line-height: 1.5;
5
+ }
6
+
7
+ .docs-sidebar-list {
8
+ margin: 0;
9
+ padding: 0;
10
+ list-style: none;
11
+ }
12
+
13
+ /* The bar of the current page sits in the gutter, so every label lines up. */
14
+ .docs-sidebar-link {
15
+ display: block;
16
+ margin-left: -0.75rem;
17
+ padding: 0.25rem 0 0.25rem calc(0.75rem - 2px);
18
+ border-left: 2px solid transparent;
19
+ color: var(--color-foreground);
20
+ text-decoration: none;
21
+ }
22
+
23
+ .docs-sidebar-link:hover {
24
+ color: var(--color-foreground-bright);
25
+ }
26
+
27
+ .docs-sidebar-link[aria-current='page'] {
28
+ border-color: var(--color-foreground-bright);
29
+ color: var(--color-foreground-bright);
30
+ font-weight: 500;
31
+ }
32
+
33
+ .docs-sidebar-chapter {
34
+ margin-top: 1rem;
35
+ }
36
+
37
+ .docs-sidebar-chapter + li:not(.docs-sidebar-chapter) {
38
+ margin-top: 1rem;
39
+ }
40
+
41
+ .docs-sidebar-chapter-title {
42
+ margin: 0 0 0.25rem;
43
+ font-size: 0.75rem;
44
+ font-weight: 500;
45
+ color: var(--color-foreground-muted);
46
+ }
47
+
48
+ .docs-sidebar-section {
49
+ margin-top: 1rem;
50
+ padding-top: 0.75rem;
51
+ border-top: 1px solid var(--color-border);
52
+ }
53
+
54
+ .docs-sidebar-section-title {
55
+ margin: 0 0 0.25rem;
56
+ color: var(--color-foreground-bright);
57
+ font-weight: 600;
58
+ }
59
+ }