@jaamd/jaad 0.11.0 → 1.0.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 (138) hide show
  1. package/README.md +8 -2
  2. package/dist/jaad.css +232 -347
  3. package/docs/00-overview.md +25 -0
  4. package/docs/01-getting-started/01-installation.md +216 -0
  5. package/docs/01-getting-started/02-pages.md +109 -0
  6. package/docs/01-getting-started/03-deployment.md +100 -0
  7. package/docs/02-configurations/01-reference.md +215 -0
  8. package/docs/02-configurations/02-frontmatter.md +105 -0
  9. package/docs/02-configurations/03-i18n.md +210 -0
  10. package/docs/02-configurations/04-search.md +58 -0
  11. package/docs/02-configurations/05-themes.md +104 -0
  12. package/docs/02-configurations/06-styles.md +156 -0
  13. package/docs/02-configurations/07-custom-pages.md +119 -0
  14. package/docs/02-configurations/08-advanced-layout.md +139 -0
  15. package/docs/03-upgrading.md +208 -0
  16. package/index.ts +8 -22
  17. package/package.json +10 -9
  18. package/site.ts +18 -1
  19. package/src/config.ts +25 -176
  20. package/src/{advanced.ts → docs/advanced.ts} +6 -4
  21. package/src/docs/collection.ts +109 -0
  22. package/src/docs/components/Breadcrumbs.astro +29 -0
  23. package/src/docs/components/DocsMobileNav.astro +90 -0
  24. package/src/docs/components/DocsNavTree.astro +57 -0
  25. package/src/docs/components/DocsPage.astro +66 -0
  26. package/src/{components/docs → docs/components}/DocsPageNav.astro +4 -4
  27. package/src/docs/components/DocsSidebarLeft.astro +53 -0
  28. package/src/{components/docs → docs/components}/PageActions.astro +10 -8
  29. package/src/{components/docs → docs/components}/StructuredData.astro +3 -3
  30. package/src/{components/docs → docs/components}/TableOfContents.astro +20 -22
  31. package/src/docs/config.ts +167 -0
  32. package/src/docs/content.ts +36 -0
  33. package/src/docs/endpoints.ts +89 -0
  34. package/src/docs/integration.ts +249 -0
  35. package/src/{layouts → docs/layouts}/DefaultDocsFrame.astro +7 -5
  36. package/src/{layouts → docs/layouts}/Docs.astro +44 -43
  37. package/src/docs/layouts/Page.astro +18 -0
  38. package/src/docs/link-plugins.ts +52 -0
  39. package/src/docs/links.ts +42 -0
  40. package/src/{locale-tree.ts → docs/locale-tree.ts} +1 -1
  41. package/src/docs/model/ids.ts +87 -0
  42. package/src/docs/model/rendered.ts +185 -0
  43. package/src/docs/model/tree.ts +124 -0
  44. package/src/docs/model/validate.ts +75 -0
  45. package/src/docs/navigation.ts +75 -0
  46. package/src/{routes → docs/routes}/404.astro +1 -1
  47. package/src/docs/routes/docs-index.astro +19 -0
  48. package/src/docs/routes/docs-slug.astro +54 -0
  49. package/src/{routes → docs/routes}/docs-slug.md.ts +8 -10
  50. package/src/{routes → docs/routes}/llms-locale.txt.ts +1 -1
  51. package/src/{routes → docs/routes}/llms.txt.ts +1 -1
  52. package/src/{routes → docs/routes}/search-index-locale.json.ts +1 -1
  53. package/src/{routes → docs/routes}/search-index.json.ts +1 -1
  54. package/src/docs/runtime.ts +17 -0
  55. package/src/docs/search-index.ts +39 -0
  56. package/src/docs/styles/components/breadcrumbs.css +26 -0
  57. package/src/docs/styles/components/docs-mobile-nav.css +47 -0
  58. package/src/docs/styles/components/docs-page-nav.css +41 -0
  59. package/src/docs/styles/components/docs-sidebar.css +59 -0
  60. package/src/docs/styles/components/page-actions.css +51 -0
  61. package/src/docs/styles/components/toc.css +38 -0
  62. package/src/docs/styles/components.css +7 -0
  63. package/src/docs/styles/docs.css +81 -0
  64. package/src/{@types/docs.ts → docs/types.ts} +8 -16
  65. package/src/{urls.ts → docs/urls.ts} +2 -16
  66. package/src/env.d.ts +1 -1
  67. package/src/integration.ts +15 -249
  68. package/src/{components → site/components}/Footer.astro +1 -1
  69. package/src/{components → site/components}/Header.astro +14 -11
  70. package/src/{components → site/components}/LocaleSwitcher.astro +2 -2
  71. package/src/{components → site/components}/NavMobile.astro +1 -1
  72. package/src/{components → site/components}/SearchPalette.astro +160 -79
  73. package/src/{components → site/components}/ThemeSwitcher.astro +1 -1
  74. package/src/site/config.ts +149 -0
  75. package/src/{infer.ts → site/infer.ts} +3 -3
  76. package/src/site/integration.ts +59 -0
  77. package/src/{layouts/Page.astro → site/layouts/Shell.astro} +6 -3
  78. package/src/{locales.ts → site/locales.ts} +9 -1
  79. package/src/{docs-config.ts → site/runtime.ts} +1 -12
  80. package/src/site/search.ts +227 -0
  81. package/src/{social.ts → site/social.ts} +2 -2
  82. package/src/site/styles/components/header.css +31 -0
  83. package/src/{styles → site/styles}/components/search.css +21 -1
  84. package/src/{styles → site/styles}/components/shell.css +4 -0
  85. package/src/site/styles/components.css +10 -0
  86. package/src/{styles → site/styles}/global.css +4 -26
  87. package/src/{themes/index.ts → site/themes.ts} +1 -1
  88. package/src/{ui → site/ui}/en.ts +3 -5
  89. package/src/{ui → site/ui}/it.ts +2 -4
  90. package/src/site/urls.ts +14 -0
  91. package/src/{virtual.ts → site/virtual.ts} +12 -18
  92. package/src/styles.css +5 -0
  93. package/src/collection.ts +0 -97
  94. package/src/components/ScrollToTop.astro +0 -43
  95. package/src/components/docs/Breadcrumbs.astro +0 -36
  96. package/src/components/docs/DocsMobileNav.astro +0 -93
  97. package/src/components/docs/DocsPage.astro +0 -68
  98. package/src/components/docs/DocsSidebarLeft.astro +0 -82
  99. package/src/content.ts +0 -42
  100. package/src/navigation.ts +0 -97
  101. package/src/routes/docs-index.astro +0 -19
  102. package/src/routes/docs-slug.astro +0 -58
  103. package/src/styles/components/breadcrumbs.css +0 -13
  104. package/src/styles/components/docs-mobile-nav.css +0 -9
  105. package/src/styles/components/docs-page-nav.css +0 -31
  106. package/src/styles/components/docs-sidebar.css +0 -42
  107. package/src/styles/components/header.css +0 -20
  108. package/src/styles/components/page-actions.css +0 -13
  109. package/src/styles/components/scroll-to-top.css +0 -11
  110. package/src/styles/components/toc.css +0 -21
  111. package/src/styles/docs.css +0 -112
  112. package/src/utils/docs.ts +0 -300
  113. package/src/utils/endpoints.ts +0 -80
  114. package/src/utils/search.ts +0 -83
  115. /package/src/{styles → docs/styles}/components/docs-page.css +0 -0
  116. /package/src/{styles → docs/styles}/jaamd.css +0 -0
  117. /package/src/{components → site/components}/Head.astro +0 -0
  118. /package/src/{components → site/components}/Nav.astro +0 -0
  119. /package/src/{components → site/components}/SocialLinks.astro +0 -0
  120. /package/src/{components → site/components}/locale-switcher.ts +0 -0
  121. /package/src/{fonts → site/fonts}/OFL.txt +0 -0
  122. /package/src/{fonts → site/fonts}/inter.woff2 +0 -0
  123. /package/src/{fonts → site/fonts}/merriweather-italic.woff2 +0 -0
  124. /package/src/{fonts → site/fonts}/merriweather.woff2 +0 -0
  125. /package/src/{fonts.ts → site/fonts.ts} +0 -0
  126. /package/src/{forges.ts → site/forges.ts} +0 -0
  127. /package/src/{utils → site}/helpers.ts +0 -0
  128. /package/src/{styles → site/styles}/components/footer.css +0 -0
  129. /package/src/{styles → site/styles}/components/locale-switcher.css +0 -0
  130. /package/src/{styles → site/styles}/components/nav-mobile.css +0 -0
  131. /package/src/{styles → site/styles}/components/nav.css +0 -0
  132. /package/src/{styles → site/styles}/components/not-found.css +0 -0
  133. /package/src/{styles → site/styles}/components/social-links.css +0 -0
  134. /package/src/{styles → site/styles}/components/theme-switcher.css +0 -0
  135. /package/src/{styles → site/styles}/jaamd-forward.css +0 -0
  136. /package/src/{styles → site/styles}/jaamd-reverse.css +0 -0
  137. /package/src/{styles → site/styles}/tailwind.css +0 -0
  138. /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.docsLocales
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 { JaadResolvedConfig } from './config.ts'
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: JaadResolvedConfig): SocialLink[] {
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-80 overflow-y-auto p-2;
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
  }
@@ -28,6 +28,10 @@
28
28
  @apply bg-background text-foreground flex min-h-screen flex-col text-[14px] leading-[1.6];
29
29
  }
30
30
 
31
+ .jaad-body {
32
+ padding-top: var(--jaad-header-height);
33
+ }
34
+
31
35
  .jaad-icon {
32
36
  @apply h-5 w-5;
33
37
  }
@@ -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
- --jaad-chrome-width: var(--jaad-content-width);
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: 8rem;
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);
@@ -1,4 +1,4 @@
1
- import { themes } from '@jaamd/jaamd/themes'
1
+ import { themes } from '@jaamd/astro/themes'
2
2
 
3
3
  export interface Preset {
4
4
  /** Shiki theme, or a light/dark pair, for code blocks. */
@@ -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
- scrollTop: 'Scroll to top',
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 This Page',
26
+ 'toc.title': 'On this page',
29
27
  } as const
@@ -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
- scrollTop: 'Torna in cima',
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',
@@ -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
- const DOCS_FRAME_ID = 'virtual:jaad/docs-frame'
19
- const RESOLVED_DOCS_FRAME = '\0' + DOCS_FRAME_ID
16
+ export interface SiteStylesheets {
17
+ user: string | null
18
+ theme: string | null
19
+ bridge: string
20
+ tailwind: string | null
21
+ }
20
22
 
21
- export function jaadVirtualPlugin(
22
- config: JaadResolvedConfig,
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 userCss ?? RESOLVED_USER_CSS
35
- if (source === THEME_CSS_ID) return themeCss ?? RESOLVED_THEME_CSS
36
- if (source === BRIDGE_CSS_ID) return bridgeCss
37
- if (source === TAILWIND_CSS_ID) return tailwindCss ?? RESOLVED_TAILWIND_CSS
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
@@ -0,0 +1,5 @@
1
+ /* The package stylesheet: Tailwind, the site's components, the docs', then the tokens. */
2
+ @import 'tailwindcss' source(none);
3
+ @import './site/styles/components.css';
4
+ @import './docs/styles/components.css';
5
+ @import './site/styles/global.css';
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
- }