@jaamd/jaad 0.11.0 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/jaad.css +232 -347
- package/docs/00-overview.md +25 -0
- package/docs/01-getting-started/01-installation.md +216 -0
- package/docs/01-getting-started/02-pages.md +109 -0
- package/docs/01-getting-started/03-deployment.md +100 -0
- package/docs/02-configurations/01-reference.md +215 -0
- package/docs/02-configurations/02-frontmatter.md +105 -0
- package/docs/02-configurations/03-i18n.md +210 -0
- package/docs/02-configurations/04-search.md +58 -0
- package/docs/02-configurations/05-themes.md +104 -0
- package/docs/02-configurations/06-styles.md +156 -0
- package/docs/02-configurations/07-custom-pages.md +119 -0
- package/docs/02-configurations/08-advanced-layout.md +139 -0
- package/docs/03-upgrading.md +208 -0
- package/index.ts +8 -22
- package/package.json +10 -9
- package/site.ts +18 -1
- package/src/config.ts +25 -176
- package/src/{advanced.ts → docs/advanced.ts} +6 -4
- package/src/docs/collection.ts +109 -0
- package/src/docs/components/Breadcrumbs.astro +29 -0
- package/src/docs/components/DocsMobileNav.astro +90 -0
- package/src/docs/components/DocsNavTree.astro +57 -0
- package/src/docs/components/DocsPage.astro +66 -0
- package/src/{components/docs → docs/components}/DocsPageNav.astro +4 -4
- package/src/docs/components/DocsSidebarLeft.astro +53 -0
- package/src/{components/docs → docs/components}/PageActions.astro +10 -8
- package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
- package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
- package/src/docs/config.ts +167 -0
- package/src/docs/content.ts +36 -0
- package/src/docs/endpoints.ts +89 -0
- package/src/docs/integration.ts +249 -0
- package/src/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
- package/src/{layouts → docs/layouts}/Docs.astro +44 -43
- package/src/docs/layouts/Page.astro +18 -0
- package/src/docs/link-plugins.ts +52 -0
- package/src/docs/links.ts +42 -0
- package/src/{locale-tree.ts → docs/locale-tree.ts} +1 -1
- package/src/docs/model/ids.ts +87 -0
- package/src/docs/model/rendered.ts +185 -0
- package/src/docs/model/tree.ts +124 -0
- package/src/docs/model/validate.ts +75 -0
- package/src/docs/navigation.ts +75 -0
- package/src/{routes → docs/routes}/404.astro +1 -1
- package/src/docs/routes/docs-index.astro +19 -0
- package/src/docs/routes/docs-slug.astro +54 -0
- package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
- package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
- package/src/{routes → docs/routes}/llms.txt.ts +1 -1
- package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
- package/src/{routes → docs/routes}/search-index.json.ts +1 -1
- package/src/docs/runtime.ts +17 -0
- package/src/docs/search-index.ts +39 -0
- package/src/docs/styles/components/breadcrumbs.css +26 -0
- package/src/docs/styles/components/docs-mobile-nav.css +47 -0
- package/src/docs/styles/components/docs-page-nav.css +41 -0
- package/src/docs/styles/components/docs-sidebar.css +59 -0
- package/src/docs/styles/components/page-actions.css +51 -0
- package/src/docs/styles/components/toc.css +38 -0
- package/src/docs/styles/components.css +7 -0
- package/src/docs/styles/docs.css +81 -0
- package/src/{@types/docs.ts → docs/types.ts} +8 -16
- package/src/{urls.ts → docs/urls.ts} +2 -16
- package/src/env.d.ts +1 -1
- package/src/integration.ts +15 -249
- package/src/{components → site/components}/Footer.astro +1 -1
- package/src/{components → site/components}/Header.astro +14 -11
- package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
- package/src/{components → site/components}/NavMobile.astro +1 -1
- package/src/{components → site/components}/SearchPalette.astro +160 -79
- package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
- package/src/site/config.ts +149 -0
- package/src/{infer.ts → site/infer.ts} +3 -3
- package/src/site/integration.ts +59 -0
- package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
- package/src/{locales.ts → site/locales.ts} +9 -1
- package/src/{docs-config.ts → site/runtime.ts} +1 -12
- package/src/site/search.ts +227 -0
- package/src/{social.ts → site/social.ts} +2 -2
- package/src/site/styles/components/header.css +31 -0
- package/src/{styles → site/styles}/components/search.css +21 -1
- package/src/{styles → site/styles}/components/shell.css +4 -0
- package/src/site/styles/components.css +10 -0
- package/src/{styles → site/styles}/global.css +4 -26
- package/src/{themes/index.ts → site/themes.ts} +1 -1
- package/src/{ui → site/ui}/en.ts +3 -5
- package/src/{ui → site/ui}/it.ts +2 -4
- package/src/site/urls.ts +14 -0
- package/src/{virtual.ts → site/virtual.ts} +12 -18
- package/src/styles.css +5 -0
- package/src/collection.ts +0 -97
- package/src/components/ScrollToTop.astro +0 -43
- package/src/components/docs/Breadcrumbs.astro +0 -36
- package/src/components/docs/DocsMobileNav.astro +0 -93
- package/src/components/docs/DocsPage.astro +0 -68
- package/src/components/docs/DocsSidebarLeft.astro +0 -82
- package/src/content.ts +0 -42
- package/src/navigation.ts +0 -97
- package/src/routes/docs-index.astro +0 -19
- package/src/routes/docs-slug.astro +0 -58
- package/src/styles/components/breadcrumbs.css +0 -13
- package/src/styles/components/docs-mobile-nav.css +0 -9
- package/src/styles/components/docs-page-nav.css +0 -31
- package/src/styles/components/docs-sidebar.css +0 -42
- package/src/styles/components/header.css +0 -20
- package/src/styles/components/page-actions.css +0 -13
- package/src/styles/components/scroll-to-top.css +0 -11
- package/src/styles/components/toc.css +0 -21
- package/src/styles/docs.css +0 -112
- package/src/utils/docs.ts +0 -300
- package/src/utils/endpoints.ts +0 -80
- package/src/utils/search.ts +0 -83
- /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
- /package/src/{styles → docs/styles}/jaamd.css +0 -0
- /package/src/{components → site/components}/Head.astro +0 -0
- /package/src/{components → site/components}/Nav.astro +0 -0
- /package/src/{components → site/components}/SocialLinks.astro +0 -0
- /package/src/{components → site/components}/locale-switcher.ts +0 -0
- /package/src/{fonts → site/fonts}/OFL.txt +0 -0
- /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
- /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
- /package/src/{fonts.ts → site/fonts.ts} +0 -0
- /package/src/{forges.ts → site/forges.ts} +0 -0
- /package/src/{utils → site}/helpers.ts +0 -0
- /package/src/{styles → site/styles}/components/footer.css +0 -0
- /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
- /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
- /package/src/{styles → site/styles}/components/nav.css +0 -0
- /package/src/{styles → site/styles}/components/not-found.css +0 -0
- /package/src/{styles → site/styles}/components/social-links.css +0 -0
- /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
- /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
- /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
- /package/src/{styles → site/styles}/tailwind.css +0 -0
- /package/src/{ui.ts → site/ui.ts} +0 -0
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import type { Locale } from './locales.ts'
|
|
2
|
-
import type { DocsUrlOptions } from './urls.ts'
|
|
3
2
|
import config from 'virtual:jaad/config'
|
|
4
3
|
import * as locales from './locales.ts'
|
|
5
4
|
import { uiString, type UiKey } from './ui.ts'
|
|
6
5
|
|
|
7
6
|
/** The virtual config, read once. */
|
|
8
|
-
export const LOCALES: Locale[] = config.
|
|
7
|
+
export const LOCALES: Locale[] = config.locales
|
|
9
8
|
export const DEFAULT_LOCALE: string = config.defaultLocale
|
|
10
9
|
|
|
11
|
-
/** The four fields every href builder takes, in one place. */
|
|
12
|
-
export function docsUrls(locale?: string): DocsUrlOptions {
|
|
13
|
-
return {
|
|
14
|
-
docsBase: config.docsBase,
|
|
15
|
-
deploymentBase: import.meta.env.BASE_URL,
|
|
16
|
-
locale,
|
|
17
|
-
defaultLocale: DEFAULT_LOCALE,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
10
|
export const localePrefix = (locale?: string) => locales.localePrefix(locale, DEFAULT_LOCALE)
|
|
22
11
|
|
|
23
12
|
export const buildLocales = () => locales.routedLocales(LOCALES)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/** The search index of one locale, as `search-index.json` serves it. */
|
|
2
|
+
export interface SearchIndex {
|
|
3
|
+
/** Empty when the docs are not split into sections. */
|
|
4
|
+
sections: { key: string; label: string }[]
|
|
5
|
+
pages: SearchPage[]
|
|
6
|
+
entries: SearchEntry[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SearchPage {
|
|
10
|
+
title: string
|
|
11
|
+
href: string
|
|
12
|
+
section?: string
|
|
13
|
+
/** Display title of the chapter. */
|
|
14
|
+
chapter?: string
|
|
15
|
+
keywords?: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The text under one heading, or before the first one. */
|
|
19
|
+
export interface SearchEntry {
|
|
20
|
+
/** Position in `pages`. */
|
|
21
|
+
page: number
|
|
22
|
+
heading?: string
|
|
23
|
+
anchor?: string
|
|
24
|
+
text: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchHit {
|
|
28
|
+
entry: SearchEntry
|
|
29
|
+
href: string
|
|
30
|
+
score: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A page and the headings inside it that matched. */
|
|
34
|
+
export interface SearchGroup {
|
|
35
|
+
page: SearchPage
|
|
36
|
+
score: number
|
|
37
|
+
/** Set when the text before the first heading matched. */
|
|
38
|
+
intro?: SearchEntry
|
|
39
|
+
hits: SearchHit[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MAX_GROUPS = 8
|
|
43
|
+
const MAX_HITS = 3
|
|
44
|
+
const PAGES_PER_SECTION = 4
|
|
45
|
+
|
|
46
|
+
/** Page fields count fully on the text before the first heading, and as
|
|
47
|
+
* context everywhere else. */
|
|
48
|
+
const WEIGHTS = {
|
|
49
|
+
title: { intro: 8, heading: 2 },
|
|
50
|
+
keywords: { intro: 4, heading: 1 },
|
|
51
|
+
chapter: { intro: 2, heading: 1 },
|
|
52
|
+
heading: 6,
|
|
53
|
+
text: 1,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const WORD = /[\p{L}\p{M}\p{N}]+/gu
|
|
57
|
+
|
|
58
|
+
/** Lowercase, without accents. */
|
|
59
|
+
export function fold(text: string): string {
|
|
60
|
+
return text.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The words of a query, folded. */
|
|
64
|
+
export function queryTerms(query: string): string[] {
|
|
65
|
+
return fold(query).match(WORD) ?? []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const words = (text: string | undefined) => (text ? (fold(text).match(WORD) ?? []) : [])
|
|
69
|
+
|
|
70
|
+
interface Prepared {
|
|
71
|
+
title: string[]
|
|
72
|
+
keywords: string[]
|
|
73
|
+
chapter: string[]
|
|
74
|
+
heading: string[]
|
|
75
|
+
text: string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The folded words of each entry, computed on first use. */
|
|
79
|
+
const prepared = new WeakMap<SearchEntry, Prepared>()
|
|
80
|
+
|
|
81
|
+
function prepare(entry: SearchEntry, page: SearchPage): Prepared {
|
|
82
|
+
let found = prepared.get(entry)
|
|
83
|
+
if (!found) {
|
|
84
|
+
found = {
|
|
85
|
+
title: words(page.title),
|
|
86
|
+
keywords: words(page.keywords?.join(' ')),
|
|
87
|
+
chapter: words(page.chapter),
|
|
88
|
+
heading: words(entry.heading),
|
|
89
|
+
text: words(entry.text),
|
|
90
|
+
}
|
|
91
|
+
prepared.set(entry, found)
|
|
92
|
+
}
|
|
93
|
+
return found
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 2 for a whole word, 1 for a word it starts, 0 for none. */
|
|
97
|
+
function match(fieldWords: string[], term: string): number {
|
|
98
|
+
let best = 0
|
|
99
|
+
for (const word of fieldWords) {
|
|
100
|
+
if (word === term) return 2
|
|
101
|
+
if (best === 0 && word.startsWith(term)) best = 1
|
|
102
|
+
}
|
|
103
|
+
return best
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The score of an entry, null unless every term is in it or in its page.
|
|
107
|
+
* `own` tells whether the heading or its text matched, not just the page. */
|
|
108
|
+
function scoreEntry(fields: Prepared, terms: string[], intro: boolean): { score: number; own: boolean } | null {
|
|
109
|
+
const place = intro ? 'intro' : 'heading'
|
|
110
|
+
let score = 0
|
|
111
|
+
let own = false
|
|
112
|
+
|
|
113
|
+
for (const term of terms) {
|
|
114
|
+
const candidates: [number, number, boolean][] = [
|
|
115
|
+
[match(fields.title, term), WEIGHTS.title[place], false],
|
|
116
|
+
[match(fields.keywords, term), WEIGHTS.keywords[place], false],
|
|
117
|
+
[match(fields.chapter, term), WEIGHTS.chapter[place], false],
|
|
118
|
+
[match(fields.heading, term), WEIGHTS.heading, true],
|
|
119
|
+
[match(fields.text, term), WEIGHTS.text, true],
|
|
120
|
+
]
|
|
121
|
+
let best = 0
|
|
122
|
+
for (const [quality, weight, isOwn] of candidates) {
|
|
123
|
+
if (quality === 0) continue
|
|
124
|
+
best = Math.max(best, quality * weight)
|
|
125
|
+
if (isOwn) own = true
|
|
126
|
+
}
|
|
127
|
+
if (best === 0) return null
|
|
128
|
+
score += best
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { score, own }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const entryHref = (page: SearchPage, entry: SearchEntry) => (entry.anchor ? `${page.href}#${entry.anchor}` : page.href)
|
|
135
|
+
|
|
136
|
+
/** Pages ranked by their best entry, each with the headings that matched.
|
|
137
|
+
* Every word must appear; a word also matches the words it starts. */
|
|
138
|
+
export function search(index: SearchIndex, query: string, section?: string): SearchGroup[] {
|
|
139
|
+
const terms = queryTerms(query)
|
|
140
|
+
if (terms.length === 0) return []
|
|
141
|
+
|
|
142
|
+
const groups = new Map<number, SearchGroup>()
|
|
143
|
+
for (const entry of index.entries) {
|
|
144
|
+
const page = index.pages[entry.page]
|
|
145
|
+
if (section && page.section !== section) continue
|
|
146
|
+
|
|
147
|
+
const intro = entry.heading === undefined
|
|
148
|
+
const scored = scoreEntry(prepare(entry, page), terms, intro)
|
|
149
|
+
if (!scored) continue
|
|
150
|
+
|
|
151
|
+
let group = groups.get(entry.page)
|
|
152
|
+
if (!group) {
|
|
153
|
+
group = { page, score: 0, hits: [] }
|
|
154
|
+
groups.set(entry.page, group)
|
|
155
|
+
}
|
|
156
|
+
group.score = Math.max(group.score, scored.score)
|
|
157
|
+
if (!scored.own) continue
|
|
158
|
+
if (intro) group.intro = entry
|
|
159
|
+
else group.hits.push({ entry, href: entryHref(page, entry), score: scored.score })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [...groups.values()]
|
|
163
|
+
.toSorted((a, b) => b.score - a.score)
|
|
164
|
+
.slice(0, MAX_GROUPS)
|
|
165
|
+
.map((group) => ({ ...group, hits: group.hits.toSorted((a, b) => b.score - a.score).slice(0, MAX_HITS) }))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What an empty query shows: the pages outside the sections, then the first
|
|
169
|
+
* pages of each section; without sections, the first pages of the docs. */
|
|
170
|
+
export function openingPages(index: SearchIndex, section?: string): { label?: string; pages: SearchPage[] }[] {
|
|
171
|
+
if (index.sections.length === 0) return [{ pages: index.pages.slice(0, MAX_GROUPS) }]
|
|
172
|
+
|
|
173
|
+
const loose = section ? [] : index.pages.filter((page) => !page.section).slice(0, PAGES_PER_SECTION)
|
|
174
|
+
return [
|
|
175
|
+
{ pages: loose },
|
|
176
|
+
...index.sections
|
|
177
|
+
.filter(({ key }) => !section || key === section)
|
|
178
|
+
.map(({ key, label }) => ({
|
|
179
|
+
label,
|
|
180
|
+
pages: index.pages.filter((page) => page.section === key).slice(0, PAGES_PER_SECTION),
|
|
181
|
+
})),
|
|
182
|
+
].filter((group) => group.pages.length > 0)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The text around the first matching word, or its start when none matches,
|
|
186
|
+
* ellipsised where cut. */
|
|
187
|
+
export function getSnippet(text: string, terms: string[], before = 40, after = 100): string {
|
|
188
|
+
const parts = markWords(text, terms)
|
|
189
|
+
const first = parts.findIndex((part) => part.match)
|
|
190
|
+
const start = parts.slice(0, Math.max(first, 0)).reduce((length, part) => length + part.text.length, 0)
|
|
191
|
+
const from = Math.max(0, start - before)
|
|
192
|
+
const to = Math.min(text.length, start + after)
|
|
193
|
+
return (from > 0 ? '…' : '') + text.slice(from, to).trim() + (to < text.length ? '…' : '')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** How many characters of `word` fold into the first `length` folded ones,
|
|
197
|
+
* accents on the last of them included. */
|
|
198
|
+
function foldedPrefix(word: string, length: number): number {
|
|
199
|
+
let folded = 0
|
|
200
|
+
let at = 0
|
|
201
|
+
for (const char of word) {
|
|
202
|
+
const size = fold(char).length
|
|
203
|
+
if (folded >= length && size > 0) break
|
|
204
|
+
folded += size
|
|
205
|
+
at += char.length
|
|
206
|
+
}
|
|
207
|
+
return at
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Splits text around the words the terms start, marking the matched starts.
|
|
211
|
+
* Case and accents are ignored. */
|
|
212
|
+
export function markWords(text: string, terms: string[]): { text: string; match: boolean }[] {
|
|
213
|
+
const parts: { text: string; match: boolean }[] = []
|
|
214
|
+
let last = 0
|
|
215
|
+
for (const word of text.matchAll(WORD)) {
|
|
216
|
+
const folded = fold(word[0])
|
|
217
|
+
const term = terms.find((candidate) => folded.startsWith(candidate))
|
|
218
|
+
if (!term) continue
|
|
219
|
+
|
|
220
|
+
const end = word.index + foldedPrefix(word[0], term.length)
|
|
221
|
+
if (word.index > last) parts.push({ text: text.slice(last, word.index), match: false })
|
|
222
|
+
parts.push({ text: text.slice(word.index, end), match: true })
|
|
223
|
+
last = end
|
|
224
|
+
}
|
|
225
|
+
if (last < text.length) parts.push({ text: text.slice(last), match: false })
|
|
226
|
+
return parts
|
|
227
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SiteResolvedConfig } from './config.ts'
|
|
2
2
|
import { FORGES, forgeForUrl } from './forges.ts'
|
|
3
3
|
|
|
4
4
|
export interface SocialLink {
|
|
@@ -11,7 +11,7 @@ export interface SocialLink {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** The configured links, plus the inferred repository unless already listed. */
|
|
14
|
-
export function buildSocialLinks(config:
|
|
14
|
+
export function buildSocialLinks(config: SiteResolvedConfig): SocialLink[] {
|
|
15
15
|
const links: SocialLink[] = []
|
|
16
16
|
|
|
17
17
|
for (const [key, value] of Object.entries(config.social)) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* Fixed to the top of the window: only the page scrolls below it. */
|
|
3
|
+
.jaad-header {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0 0 auto;
|
|
6
|
+
z-index: 40;
|
|
7
|
+
height: var(--jaad-header-height);
|
|
8
|
+
background: var(--color-background);
|
|
9
|
+
}
|
|
10
|
+
.jaad-header-inner {
|
|
11
|
+
height: 100%;
|
|
12
|
+
max-width: var(--jaad-chrome-width);
|
|
13
|
+
margin-inline: auto;
|
|
14
|
+
padding-inline: 1rem;
|
|
15
|
+
}
|
|
16
|
+
.jaad-header-row {
|
|
17
|
+
@apply flex h-full items-center gap-4;
|
|
18
|
+
}
|
|
19
|
+
.jaad-brand {
|
|
20
|
+
@apply text-foreground-bright text-xl;
|
|
21
|
+
}
|
|
22
|
+
.jaad-brand-link {
|
|
23
|
+
@apply text-primary no-underline;
|
|
24
|
+
}
|
|
25
|
+
.jaad-logo {
|
|
26
|
+
@apply h-7;
|
|
27
|
+
}
|
|
28
|
+
.jaad-header-actions {
|
|
29
|
+
@apply ml-auto flex items-center gap-2;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -48,8 +48,24 @@
|
|
|
48
48
|
@apply bg-background-secondary text-foreground-muted shrink-0 cursor-pointer rounded px-1.5 py-0.5 text-xs;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
.jaad-search-filters {
|
|
52
|
+
@apply border-border flex flex-wrap gap-1.5 border-b px-4 py-2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.jaad-search-filter {
|
|
56
|
+
@apply border-border text-foreground-muted hover:text-foreground cursor-pointer rounded-full border px-2.5 py-0.5 text-xs transition-colors;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.jaad-search-filter[aria-pressed='true'] {
|
|
60
|
+
@apply border-primary/40 bg-primary/10 text-foreground-bright;
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
.jaad-search-results {
|
|
52
|
-
@apply max-h-
|
|
64
|
+
@apply max-h-96 overflow-y-auto p-2;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.jaad-search-group {
|
|
68
|
+
@apply text-foreground-muted px-3 pt-3 pb-1 text-xs font-semibold tracking-wide uppercase;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
.jaad-search-footer {
|
|
@@ -72,6 +88,10 @@
|
|
|
72
88
|
@apply bg-primary/10 text-foreground-bright;
|
|
73
89
|
}
|
|
74
90
|
|
|
91
|
+
.jaad-search-result-heading {
|
|
92
|
+
@apply border-border ml-3 rounded-l-none border-l py-2;
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
.jaad-search-result-head {
|
|
76
96
|
@apply flex items-center gap-2;
|
|
77
97
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
@import './components/shell.css';
|
|
2
|
+
@import './components/header.css';
|
|
3
|
+
@import './components/footer.css';
|
|
4
|
+
@import './components/nav.css';
|
|
5
|
+
@import './components/nav-mobile.css';
|
|
6
|
+
@import './components/social-links.css';
|
|
7
|
+
@import './components/theme-switcher.css';
|
|
8
|
+
@import './components/locale-switcher.css';
|
|
9
|
+
@import './components/not-found.css';
|
|
10
|
+
@import './components/search.css';
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
@import 'tailwindcss' source(none);
|
|
2
|
-
@import './components/shell.css';
|
|
3
|
-
@import './components/header.css';
|
|
4
|
-
@import './components/footer.css';
|
|
5
|
-
@import './components/nav.css';
|
|
6
|
-
@import './components/nav-mobile.css';
|
|
7
|
-
@import './components/social-links.css';
|
|
8
|
-
@import './components/theme-switcher.css';
|
|
9
|
-
@import './components/locale-switcher.css';
|
|
10
|
-
@import './components/scroll-to-top.css';
|
|
11
|
-
@import './components/not-found.css';
|
|
12
|
-
@import './components/search.css';
|
|
13
|
-
@import './components/docs-page.css';
|
|
14
|
-
@import './components/docs-sidebar.css';
|
|
15
|
-
@import './components/toc.css';
|
|
16
|
-
@import './components/docs-mobile-nav.css';
|
|
17
|
-
@import './components/docs-page-nav.css';
|
|
18
|
-
@import './components/page-actions.css';
|
|
19
|
-
@import './components/breadcrumbs.css';
|
|
20
|
-
|
|
21
1
|
@theme static {
|
|
22
2
|
/* Seeds */
|
|
23
3
|
|
|
@@ -55,18 +35,16 @@
|
|
|
55
35
|
|
|
56
36
|
/* Layout */
|
|
57
37
|
--jaad-content-width: 56rem;
|
|
58
|
-
|
|
38
|
+
/* Sidebar, page and table of contents, with their gaps and the gutters. */
|
|
39
|
+
--jaad-chrome-width: calc(var(--jaad-content-width) + 2 * var(--jaad-sidebar-width) + 6rem);
|
|
59
40
|
--jaad-sidebar-width: 16rem;
|
|
41
|
+
--jaad-header-height: 4.25rem;
|
|
60
42
|
--jaad-page-padding: 2rem;
|
|
61
|
-
|
|
62
|
-
/* Where a sidebar sits beside centred content. Derived, so moving the
|
|
63
|
-
content width moves the sidebars with it. */
|
|
64
|
-
--jaad-sidebar-offset: calc(50% - (var(--jaad-content-width) / 2) - var(--jaad-sidebar-width) - 2rem);
|
|
65
43
|
}
|
|
66
44
|
|
|
67
45
|
@layer base {
|
|
68
46
|
html {
|
|
69
|
-
scroll-padding-top:
|
|
47
|
+
scroll-padding-top: calc(var(--jaad-header-height) + 1.5rem);
|
|
70
48
|
background-color: var(--color-background);
|
|
71
49
|
color: var(--color-foreground);
|
|
72
50
|
font-family: var(--font-sans);
|
package/src/{ui → site/ui}/en.ts
RENAMED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/** The canonical UI catalogue: its keys define every catalogue JAAD accepts. */
|
|
2
2
|
export const ENGLISH_UI = {
|
|
3
3
|
'breadcrumb.label': 'Breadcrumb',
|
|
4
|
-
'breadcrumb.root': 'Docs',
|
|
5
4
|
home: 'Home',
|
|
6
5
|
'locale.change': 'Change language',
|
|
7
6
|
'nav.documentation': 'Documentation navigation',
|
|
8
|
-
'nav.jumpToSection': 'Jump to section...',
|
|
9
7
|
'nav.mobile': 'Toggle mobile menu',
|
|
10
8
|
'nav.page': 'Page navigation',
|
|
11
|
-
'nav.selectPage': 'Select page',
|
|
12
9
|
'page.copied': 'Copied!',
|
|
13
10
|
'page.copy': 'Copy page',
|
|
14
11
|
'page.edit': 'Edit this page',
|
|
15
12
|
'page.lastUpdated': 'Last updated on',
|
|
16
|
-
|
|
13
|
+
'search.all': 'All',
|
|
17
14
|
'search.close': 'close',
|
|
18
15
|
'search.empty': 'No results for',
|
|
19
16
|
'search.label': 'Search documentation',
|
|
@@ -22,8 +19,9 @@ export const ENGLISH_UI = {
|
|
|
22
19
|
'search.open': 'open',
|
|
23
20
|
'search.placeholder': 'Search documentation...',
|
|
24
21
|
'search.results': 'Search results',
|
|
22
|
+
'search.sections': 'Search in',
|
|
25
23
|
'search.trigger': 'Search docs...',
|
|
26
24
|
'theme.toggle': 'Toggle color theme',
|
|
27
25
|
'toc.label': 'Table of contents',
|
|
28
|
-
'toc.title': 'On
|
|
26
|
+
'toc.title': 'On this page',
|
|
29
27
|
} as const
|
package/src/{ui → site/ui}/it.ts
RENAMED
|
@@ -2,19 +2,16 @@ import type { UiCatalog } from '../ui.ts'
|
|
|
2
2
|
|
|
3
3
|
export const ITALIAN_UI = {
|
|
4
4
|
'breadcrumb.label': 'Percorso di navigazione',
|
|
5
|
-
'breadcrumb.root': 'Documentazione',
|
|
6
5
|
home: 'Pagina iniziale',
|
|
7
6
|
'locale.change': 'Cambia lingua',
|
|
8
7
|
'nav.documentation': 'Navigazione della documentazione',
|
|
9
|
-
'nav.jumpToSection': 'Vai alla sezione…',
|
|
10
8
|
'nav.mobile': 'Apri o chiudi il menu',
|
|
11
9
|
'nav.page': 'Navigazione tra le pagine',
|
|
12
|
-
'nav.selectPage': 'Seleziona una pagina',
|
|
13
10
|
'page.copied': 'Copiata!',
|
|
14
11
|
'page.copy': 'Copia',
|
|
15
12
|
'page.edit': 'Modifica',
|
|
16
13
|
'page.lastUpdated': 'Ultimo aggiornamento il',
|
|
17
|
-
|
|
14
|
+
'search.all': 'Tutte',
|
|
18
15
|
'search.close': 'chiudi',
|
|
19
16
|
'search.empty': 'Nessun risultato per',
|
|
20
17
|
'search.label': 'Cerca nella documentazione',
|
|
@@ -23,6 +20,7 @@ export const ITALIAN_UI = {
|
|
|
23
20
|
'search.open': 'apri',
|
|
24
21
|
'search.placeholder': 'Cerca nella documentazione…',
|
|
25
22
|
'search.results': 'Risultati della ricerca',
|
|
23
|
+
'search.sections': 'Cerca in',
|
|
26
24
|
'search.trigger': 'Cerca…',
|
|
27
25
|
'theme.toggle': 'Cambia modalità',
|
|
28
26
|
'toc.label': 'Indice dei contenuti',
|
package/src/site/urls.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Empty at the site root, otherwise one leading slash and no trailing slash. */
|
|
2
|
+
export function normaliseBasePath(value: string): string {
|
|
3
|
+
const trimmed = value.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
4
|
+
return trimmed ? `/${trimmed}` : ''
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Apply Astro's deployment base to an application-local, root-relative href. */
|
|
8
|
+
export function withDeploymentBase(href: string, deploymentBase: string): string {
|
|
9
|
+
if (!href.startsWith('/') || href.startsWith('//')) return href
|
|
10
|
+
|
|
11
|
+
const prefix = normaliseBasePath(deploymentBase)
|
|
12
|
+
if (!prefix) return href
|
|
13
|
+
return href === '/' ? `${prefix}/` : `${prefix}${href}`
|
|
14
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { JaadResolvedConfig } from './config.ts'
|
|
2
|
-
|
|
3
1
|
const MODULE_ID = 'virtual:jaad/config'
|
|
4
2
|
const RESOLVED_ID = '\0' + MODULE_ID
|
|
5
3
|
|
|
@@ -15,33 +13,29 @@ const BRIDGE_CSS_ID = 'virtual:jaad/bridge.css'
|
|
|
15
13
|
const TAILWIND_CSS_ID = 'virtual:jaad/tailwind.css'
|
|
16
14
|
const RESOLVED_TAILWIND_CSS = '\0' + TAILWIND_CSS_ID
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export interface SiteStylesheets {
|
|
17
|
+
user: string | null
|
|
18
|
+
theme: string | null
|
|
19
|
+
bridge: string
|
|
20
|
+
tailwind: string | null
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
userCss: string | null,
|
|
24
|
-
themeCss: string | null,
|
|
25
|
-
bridgeCss: string,
|
|
26
|
-
tailwindCss: string | null,
|
|
27
|
-
docsFrame: () => string,
|
|
28
|
-
) {
|
|
23
|
+
/** The resolved config, serialised, and the stylesheets the site may or may not have. */
|
|
24
|
+
export function siteVirtualPlugin(config: object, stylesheets: SiteStylesheets) {
|
|
29
25
|
return {
|
|
30
26
|
name: 'jaad:virtual',
|
|
31
27
|
resolveId(source: string) {
|
|
32
28
|
if (source === MODULE_ID) return RESOLVED_ID
|
|
33
29
|
// Redirecting to the real path lets Vite process it as ordinary CSS.
|
|
34
|
-
if (source === USER_CSS_ID) return
|
|
35
|
-
if (source === THEME_CSS_ID) return
|
|
36
|
-
if (source === BRIDGE_CSS_ID) return
|
|
37
|
-
if (source === TAILWIND_CSS_ID) return
|
|
38
|
-
if (source === DOCS_FRAME_ID) return RESOLVED_DOCS_FRAME
|
|
30
|
+
if (source === USER_CSS_ID) return stylesheets.user ?? RESOLVED_USER_CSS
|
|
31
|
+
if (source === THEME_CSS_ID) return stylesheets.theme ?? RESOLVED_THEME_CSS
|
|
32
|
+
if (source === BRIDGE_CSS_ID) return stylesheets.bridge
|
|
33
|
+
if (source === TAILWIND_CSS_ID) return stylesheets.tailwind ?? RESOLVED_TAILWIND_CSS
|
|
39
34
|
return null
|
|
40
35
|
},
|
|
41
36
|
load(id: string) {
|
|
42
37
|
if (id === RESOLVED_ID) return `export default ${JSON.stringify(config)};`
|
|
43
38
|
if (id === RESOLVED_USER_CSS || id === RESOLVED_THEME_CSS || id === RESOLVED_TAILWIND_CSS) return ''
|
|
44
|
-
if (id === RESOLVED_DOCS_FRAME) return `export { default } from ${JSON.stringify(docsFrame())};`
|
|
45
39
|
return null
|
|
46
40
|
},
|
|
47
41
|
}
|
package/src/styles.css
ADDED
package/src/collection.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
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
|
-
}
|