@master/internal-site 0.1.2 → 0.2.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 (239) hide show
  1. package/README.md +43 -0
  2. package/dist/astro.mjs +233 -47
  3. package/dist/config.d.mts +6 -0
  4. package/dist/localization-LfIbIysE.mjs +356 -0
  5. package/dist/localization.d.mts +69 -0
  6. package/dist/localization.mjs +1 -0
  7. package/package.json +12 -9
  8. package/src/astro/content-pages.ts +30 -11
  9. package/src/astro/index.ts +11 -5
  10. package/src/astro/localized-mdx.ts +45 -0
  11. package/src/astro/routes.template.ts +4 -0
  12. package/src/astro/site-context.ts +10 -2
  13. package/src/astro/style-assets.ts +107 -0
  14. package/src/benchmarks/BenchmarkBarRows.astro +3 -2
  15. package/src/benchmarks/BenchmarkBars.astro +1 -1
  16. package/src/benchmarks/BenchmarkChart.astro +5 -1
  17. package/src/benchmarks/BenchmarkChartGroup.astro +1 -1
  18. package/src/benchmarks/BenchmarkDataTable.astro +1 -1
  19. package/src/benchmarks/BenchmarkFigure.astro +2 -1
  20. package/src/benchmarks/BenchmarkMetricTable.astro +2 -2
  21. package/src/benchmarks/BenchmarkMetrics.astro +1 -1
  22. package/src/benchmarks/BenchmarkSampleSummary.astro +1 -1
  23. package/src/benchmarks/BenchmarkScrollRegion.svelte +2 -1
  24. package/src/benchmarks/BenchmarkSource.astro +1 -1
  25. package/src/benchmarks/BenchmarkStackedBars.astro +4 -3
  26. package/src/benchmarks/BenchmarkStatus.astro +3 -1
  27. package/src/benchmarks/localization.ts +22 -0
  28. package/src/benchmarks/types.ts +2 -0
  29. package/src/blog/article/BlogArticleContents.svelte +1 -1
  30. package/src/blog/article/BlogArticleEnd.astro +6 -9
  31. package/src/blog/article/BlogArticleMasthead.astro +17 -9
  32. package/src/config.ts +3 -0
  33. package/src/css/NamespaceUtilityTable.astro +1 -1
  34. package/src/css/ThemeNumberVariableTable.astro +1 -1
  35. package/src/css/foundation-data/specimen-content.ts +2 -2
  36. package/src/css/markdown.ts +64 -14
  37. package/src/demo/Demo.svelte +10 -4
  38. package/src/demo/DemoAsset.astro +6 -3
  39. package/src/demo/DemoAxes.svelte +4 -2
  40. package/src/demo/DemoCatalog.astro +9 -6
  41. package/src/demo/DemoComparison.svelte +3 -0
  42. package/src/demo/DemoContainer.svelte +8 -3
  43. package/src/demo/DemoControls.svelte +5 -1
  44. package/src/demo/DemoCopyButton.svelte +9 -4
  45. package/src/demo/DemoCopyGroup.svelte +6 -2
  46. package/src/demo/DemoExample.astro +3 -1
  47. package/src/demo/DemoFeatureSupport.svelte +5 -1
  48. package/src/demo/DemoGeneratedCSS.svelte +5 -1
  49. package/src/demo/DemoIndex.astro +6 -3
  50. package/src/demo/DemoLegend.svelte +3 -1
  51. package/src/demo/DemoMeasure.svelte +4 -2
  52. package/src/demo/DemoMedia.svelte +3 -1
  53. package/src/demo/DemoMotion.svelte +6 -3
  54. package/src/demo/DemoPaletteGroups.svelte +4 -2
  55. package/src/demo/DemoScrollArea.svelte +2 -0
  56. package/src/demo/DemoStyleComparison.astro +6 -3
  57. package/src/demo/DemoSurface.svelte +2 -0
  58. package/src/demo/DemoThemeComparison.astro +5 -2
  59. package/src/demo/DemoTokenTable.astro +5 -3
  60. package/src/demo/DemoViewTransition.astro +3 -1
  61. package/src/demo/DemoViewport.svelte +18 -11
  62. package/src/demo/DemoWaterfall.astro +4 -2
  63. package/src/demo/ProjectStyleExample.astro +4 -1
  64. package/src/demo/foundations/FoundationDialog.astro +6 -2
  65. package/src/demo/foundations/FoundationMedia.astro +1 -0
  66. package/src/demo/foundations/FoundationShrink.astro +1 -0
  67. package/src/demo/foundations/FoundationTokensView.astro +16 -5
  68. package/src/demo/foundations/Palette.svelte +11 -7
  69. package/src/demo/foundations/dialog-copy.d.ts +1 -0
  70. package/src/demo/foundations/dialog-copy.js +5 -0
  71. package/src/demo/illustration-materials.ts +1 -1
  72. package/src/demo/illustration.css +218 -0
  73. package/src/docs-shell/common/i18n.config.d.ts +1 -1
  74. package/src/docs-shell/common/i18n.config.js +4 -12
  75. package/src/docs-shell/common/locales.d.ts +8 -0
  76. package/src/docs-shell/common/locales.js +18 -0
  77. package/src/docs-shell/common/terminology.json +32 -0
  78. package/src/docs-shell/common/translations.d.ts +3 -0
  79. package/src/docs-shell/common/translations.js +294 -0
  80. package/src/docs-shell/components/ArticleHeader.astro +5 -3
  81. package/src/docs-shell/components/Callout.astro +1 -0
  82. package/src/docs-shell/components/CodeCopyButton.astro +5 -1
  83. package/src/docs-shell/components/CodeTabBar.astro +7 -3
  84. package/src/docs-shell/components/CodeTabs.astro +2 -1
  85. package/src/docs-shell/components/CodeView.astro +1 -1
  86. package/src/docs-shell/components/DocHeader.svelte +13 -17
  87. package/src/docs-shell/components/DocHeading.astro +3 -1
  88. package/src/docs-shell/components/DocMenuButton.svelte +126 -61
  89. package/src/docs-shell/components/DocSidebar.svelte +3 -2
  90. package/src/docs-shell/components/DocVersionSelect.svelte +2 -0
  91. package/src/docs-shell/components/DocumentAPIIndex.astro +1 -1
  92. package/src/docs-shell/components/DocumentChoices.astro +1 -1
  93. package/src/docs-shell/components/DocumentCodeExampleView.astro +3 -1
  94. package/src/docs-shell/components/DocumentCodeTable.astro +4 -2
  95. package/src/docs-shell/components/DocumentCopyButton.svelte +3 -0
  96. package/src/docs-shell/components/DocumentDisclosure.astro +2 -0
  97. package/src/docs-shell/components/DocumentFileTree.astro +3 -0
  98. package/src/docs-shell/components/DocumentFlow.astro +4 -1
  99. package/src/docs-shell/components/DocumentIllustration.astro +8 -12
  100. package/src/docs-shell/components/DocumentKeyList.astro +6 -4
  101. package/src/docs-shell/components/DocumentNamespaceTable.astro +5 -3
  102. package/src/docs-shell/components/DocumentOptionEntry.astro +0 -1
  103. package/src/docs-shell/components/DocumentOptionList.astro +1 -1
  104. package/src/docs-shell/components/DocumentOptions.astro +3 -2
  105. package/src/docs-shell/components/DocumentParameters.astro +1 -1
  106. package/src/docs-shell/components/DocumentPrompt.astro +2 -0
  107. package/src/docs-shell/components/DocumentValueList.astro +1 -1
  108. package/src/docs-shell/components/DocumentationIndex.svelte +8 -7
  109. package/src/docs-shell/components/ExpandContent.svelte +3 -0
  110. package/src/docs-shell/components/ExplorerView.astro +1 -0
  111. package/src/docs-shell/components/GeneratedCSS.astro +4 -1
  112. package/src/docs-shell/components/Header.svelte +1 -1
  113. package/src/docs-shell/components/InlineCode.astro +1 -0
  114. package/src/docs-shell/components/InteractingIndicator.astro +1 -0
  115. package/src/docs-shell/components/LanguageButton.svelte +1 -0
  116. package/src/docs-shell/components/LanguageSelect.svelte +2 -0
  117. package/src/docs-shell/components/Link.svelte +3 -2
  118. package/src/docs-shell/components/NotFound.astro +6 -4
  119. package/src/docs-shell/components/PMAction.astro +1 -0
  120. package/src/docs-shell/components/PageContent.svelte +1 -1
  121. package/src/docs-shell/components/PageContentDisclosure.astro +2 -1
  122. package/src/docs-shell/components/PageContentList.svelte +2 -0
  123. package/src/docs-shell/components/PageNavs.astro +3 -3
  124. package/src/docs-shell/components/SearchButton.svelte +3 -0
  125. package/src/docs-shell/components/SearchDialog.svelte +18 -16
  126. package/src/docs-shell/components/Segments.svelte +2 -0
  127. package/src/docs-shell/components/StepSection.astro +1 -2
  128. package/src/docs-shell/components/Tabs.astro +1 -0
  129. package/src/docs-shell/components/ThemeSelect.svelte +2 -0
  130. package/src/docs-shell/components/TimeAgo.svelte +6 -6
  131. package/src/docs-shell/components/markdown/Table.astro +1 -0
  132. package/src/docs-shell/components/markdown-components.ts +0 -1
  133. package/src/docs-shell/contexts/search.ts +1 -1
  134. package/src/docs-shell/factories/Page.astro +10 -5
  135. package/src/docs-shell/factories/create-page.ts +33 -6
  136. package/src/docs-shell/layouts/BlogArticle.astro +3 -11
  137. package/src/docs-shell/remark/markdown-components.js +28 -7
  138. package/src/docs-shell/remark/slug-and-toc.js +5 -7
  139. package/src/docs-shell/scripts/code.ts +5 -2
  140. package/src/docs-shell/scripts/document.ts +1 -1
  141. package/src/docs-shell/site.ts +9 -1
  142. package/src/docs-shell/types/Metadata.ts +11 -0
  143. package/src/docs-shell/utils/document-copy.d.ts +2 -0
  144. package/src/docs-shell/utils/document-copy.js +18 -0
  145. package/src/docs-shell/utils/generate-metadata.ts +5 -1
  146. package/src/docs-shell/utils/head.ts +5 -2
  147. package/src/docs-shell/utils/i18n-pathname.ts +13 -2
  148. package/src/docs-shell/utils/i18n.ts +4 -1
  149. package/src/docs-shell/utils/localized-metadata.ts +22 -0
  150. package/src/docs-shell/utils/markdown-links.ts +61 -0
  151. package/src/docs-shell/utils/markdown-tree.ts +19 -6
  152. package/src/docs-shell/utils/resolve-heading.d.ts +1 -0
  153. package/src/docs-shell/utils/resolve-heading.js +21 -0
  154. package/src/docs-shell/utils/search-pages.ts +75 -22
  155. package/src/examples/components/ArticleTransitionScene.svelte +4 -0
  156. package/src/examples/layout-system/page.astro +0 -1
  157. package/src/home/HomeBentoFrame.astro +1 -0
  158. package/src/home/HomeBentoGrid.astro +1 -0
  159. package/src/home/bento.css +167 -0
  160. package/src/home/design.css +1 -1
  161. package/src/home/hero.css +5 -0
  162. package/src/home/home.css +4 -576
  163. package/src/home/segmented-control.css +94 -0
  164. package/src/layouts/Document.astro +4 -2
  165. package/src/public/icons/json.svg +1 -0
  166. package/src/scripts/atomic-class-authoring.ts +98 -0
  167. package/src/scripts/localization.ts +239 -0
  168. package/src/scripts/mdx-sample.ts +12 -0
  169. package/src/scripts/static-social.ts +17 -3
  170. package/src/scripts/style-delivery.mjs +120 -0
  171. package/src/sections/blog.astro +1 -1
  172. package/src/sections/guide.astro +4 -3
  173. package/src/styles/blog-article.css +10 -7
  174. package/src/styles/blog-materials.css +13 -0
  175. package/src/styles/demo.css +0 -1
  176. package/src/styles/docs-shell/code.css +5 -61
  177. package/src/styles/docs-shell/documentation-search.css +1 -0
  178. package/src/styles/docs-shell/header.css +2 -73
  179. package/src/styles/docs-shell/menu.css +37 -17
  180. package/src/styles/docs-shell/page-content.css +2 -37
  181. package/src/styles/docs-shell/prose.css +18 -9
  182. package/src/styles/docs-shell/steps.css +2 -39
  183. package/src/styles/document-comparison.css +5 -2
  184. package/src/styles/documentation-illustration.css +1 -9
  185. package/src/styles/documentation-index.css +2 -1
  186. package/src/styles/documents.css +2 -4
  187. package/src/styles/interface.css +5 -3
  188. package/src/styles/system/DESIGN.md +34 -14
  189. package/src/styles/system/browser.css +21 -0
  190. package/src/styles/system/button-feedback.css +13 -0
  191. package/src/styles/system/callout.css +14 -0
  192. package/src/styles/system/chart.css +18 -0
  193. package/src/styles/system/checks.css +18 -0
  194. package/src/styles/system/code-panel.css +82 -0
  195. package/src/styles/system/code-tabs.css +40 -0
  196. package/src/styles/system/code.css +8 -85
  197. package/src/styles/system/compare.css +7 -0
  198. package/src/styles/system/controls.css +10 -109
  199. package/src/styles/system/copy.css +32 -0
  200. package/src/styles/system/data.css +10 -134
  201. package/src/styles/system/dialog.css +16 -0
  202. package/src/styles/system/disclosure.css +19 -0
  203. package/src/styles/system/expand.css +11 -0
  204. package/src/styles/system/fields.css +25 -0
  205. package/src/styles/system/header-core.css +101 -0
  206. package/src/styles/system/hint.css +5 -0
  207. package/src/styles/system/index.css +13 -0
  208. package/src/styles/system/inline-code.css +4 -0
  209. package/src/styles/system/kbd.css +4 -0
  210. package/src/styles/system/logos.css +10 -0
  211. package/src/styles/system/menu.css +14 -0
  212. package/src/styles/system/metrics.css +22 -0
  213. package/src/styles/system/pagehead.css +13 -0
  214. package/src/styles/system/pager.css +13 -0
  215. package/src/styles/system/parameters.css +17 -0
  216. package/src/styles/system/prompt.css +7 -0
  217. package/src/styles/system/prose.css +4 -75
  218. package/src/styles/system/range.css +12 -0
  219. package/src/styles/system/reading.css +44 -0
  220. package/src/styles/system/scroll.css +6 -0
  221. package/src/styles/system/search-trigger.css +10 -0
  222. package/src/styles/system/segmented.css +22 -0
  223. package/src/styles/system/sheet.css +8 -0
  224. package/src/styles/system/shell.css +8 -102
  225. package/src/styles/system/sidebar.css +14 -0
  226. package/src/styles/system/source.css +5 -0
  227. package/src/styles/system/specimens.css +3 -0
  228. package/src/styles/system/stage.css +18 -0
  229. package/src/styles/system/steps.css +51 -0
  230. package/src/styles/system/surfaces.css +8 -104
  231. package/src/styles/system/table.css +23 -0
  232. package/src/styles/system/toc.css +59 -0
  233. package/src/styles/system/tooltip.css +6 -0
  234. package/src/styles/system/tray-code.css +6 -0
  235. package/src/styles/system/tray.css +20 -0
  236. package/src/styles/system/tree.css +15 -0
  237. package/src/styles/system/waterfall.css +14 -0
  238. package/src/virtual.d.ts +5 -1
  239. package/src/blog/article/language.ts +0 -29
@@ -1,16 +1,15 @@
1
- import Slugger from 'github-slugger'
2
1
  import { visit } from 'unist-util-visit'
3
- import resolveHeading from '../utils/resolve-heading.js'
2
+ import { createHeadingResolver } from '../utils/resolve-heading.js'
4
3
  import { parse } from 'acorn'
5
4
 
6
5
  export default function remarkSlugAndTOC() {
7
6
  return (tree) => {
8
- const slugs = new Slugger()
7
+ const resolveHeading = createHeadingResolver()
9
8
  const toc = []
10
9
 
11
10
  visit(tree, 'heading', (node) => {
12
11
  const depth = node.depth
13
- if (depth !== 2 && depth !== 3) return
12
+ if (depth < 2 || depth > 6) return
14
13
 
15
14
  // Extract raw text from heading children
16
15
  const title = node.children
@@ -19,8 +18,7 @@ export default function remarkSlugAndTOC() {
19
18
  .join(' ')
20
19
  .trim()
21
20
 
22
- const id = slugs.slug(title)
23
- const resolved = resolveHeading(title, id)
21
+ const resolved = resolveHeading(title, depth)
24
22
  // Explicit IDs survive title edits and translations; they are not visible prose.
25
23
  const lastText = node.children.findLast((child) => child.type === 'text')
26
24
  if (lastText) lastText.value = lastText.value.replace(/\s+\{#[\w-]+\}$/, '')
@@ -29,7 +27,7 @@ export default function remarkSlugAndTOC() {
29
27
  node.data.id = resolved.id
30
28
  node.data.hProperties.id = resolved.id
31
29
 
32
- toc.push({
30
+ if (depth === 2 || depth === 3) toc.push({
33
31
  id: resolved.id,
34
32
  title: resolved.title,
35
33
  level: depth
@@ -1,4 +1,7 @@
1
1
  /** Code blocks render as static markup; these delegated handlers switch tabs and copy code. */
2
+ import { readSiteContext } from '../site'
3
+
4
+ const t = (text: string) => readSiteContext().translations[text] ?? text
2
5
 
3
6
  const tabChange = 'site-code-tabs:change'
4
7
  const resetTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>()
@@ -24,7 +27,7 @@ function selectTab(root: HTMLElement, name: string) {
24
27
  const copy = root.querySelector<HTMLButtonElement>('[data-code-copy]')
25
28
  if (copy) {
26
29
  copy.dataset.codeCopy = name
27
- copy.ariaLabel = copy.title = `Copy ${name}`
30
+ copy.ariaLabel = copy.title = t('Copy {name}').replace('{name}', t(name))
28
31
  }
29
32
  }
30
33
 
@@ -55,7 +58,7 @@ function showCopyStatus(button: HTMLButtonElement, status: 'idle' | 'pending' |
55
58
  const element = button.closest('.ui-copy')?.querySelector<HTMLElement>('.ui-copy-status')
56
59
  const result = status === 'copied' ? 'success' : status === 'error' ? 'error' : undefined
57
60
  if (element) {
58
- element.textContent = status === 'copied' ? `${name || 'Code'} copied` : status === 'error' ? 'Clipboard unavailable. Select the code to copy it.' : ''
61
+ element.textContent = status === 'copied' ? t('{name} copied').replace('{name}', t(name || 'Code')) : status === 'error' ? t('Clipboard unavailable. Select the code to copy it.') : ''
59
62
  if (result) element.dataset.result = result
60
63
  else delete element.dataset.result
61
64
  }
@@ -44,4 +44,4 @@ const tableSizes = new ResizeObserver(entries => {
44
44
  else table.removeAttribute('tabindex')
45
45
  }
46
46
  })
47
- document.querySelectorAll<HTMLElement>('.ui-table:not([tabindex])').forEach(table => { if (!table.closest('site-island')) tableSizes.observe(table) })
47
+ document.querySelectorAll<HTMLElement>('.ui-table:not([tabindex])').forEach(table => { if (!table.closest('astro-island')) tableSizes.observe(table) })
@@ -15,6 +15,7 @@ export interface SiteI18n {
15
15
  defaultLocale: string
16
16
  locales: readonly string[]
17
17
  localizablePathnameRoots: string[]
18
+ localizableOrigins?: Record<string, readonly string[]>
18
19
  hreflangOfLocale: Record<string, string>
19
20
  nameOfLocale: Record<string, string>
20
21
  }
@@ -47,6 +48,7 @@ export function createSite(value: SiteContextValue): Site {
47
48
  }
48
49
 
49
50
  let clientSite: Site | undefined
51
+ let clientContext: string | undefined
50
52
 
51
53
  /** Read the context Document.astro serializes for client islands. */
52
54
  export function readSiteContext(): SiteContextValue {
@@ -63,5 +65,11 @@ export function getSite(): Site {
63
65
  const value = getContext<SiteContextValue | undefined>(siteContextKey)
64
66
  if (value) return createSite(value)
65
67
  if (typeof document === 'undefined') throw new Error('getSite() must run inside a component rendered by the site')
66
- return clientSite ??= createSite(readSiteContext())
68
+ const serialized = document.getElementById(siteContextElementId)?.textContent
69
+ if (!serialized) throw new Error(`Missing #${siteContextElementId}`)
70
+ if (serialized !== clientContext) {
71
+ clientContext = serialized
72
+ clientSite = createSite(JSON.parse(serialized))
73
+ }
74
+ return clientSite!
67
75
  }
@@ -1,10 +1,21 @@
1
1
  export declare type MetadataTitle = string | { absolute: string } | { default: string, template?: string }
2
2
 
3
+ /** An article's visible cover also supplies its social cards. */
4
+ export interface ArticleCover {
5
+ src: string
6
+ alt: string
7
+ width: number
8
+ height: number
9
+ /** Optional PNG/JPEG version of the same artwork for social clients. */
10
+ socialImage?: string
11
+ }
12
+
3
13
  /** Page metadata in the shape `docs-shell/utils/head` renders. */
4
14
  export declare type Metadata = {
5
15
  title?: MetadataTitle
6
16
  description?: string
7
17
  category?: string
18
+ cover?: ArticleCover
8
19
  robots?: string | { index?: boolean, follow?: boolean }
9
20
  alternates?: { canonical?: string }
10
21
  openGraph?: { title?: string, description?: string, siteName?: string, images?: string | string[] }
@@ -0,0 +1,2 @@
1
+ /** Overlay prose fields only; executable source and technical identity stay shared. */
2
+ export declare function demoCopy<T>(frontmatter: Record<string, any>, locale: string, key: string, original: T): T
@@ -0,0 +1,18 @@
1
+ const proseFields = new Set(['title', 'description', 'caption', 'summary', 'label', 'note', 'lead', 'sourceLabel', 'resultLabel', 'alt'])
2
+
3
+ /** Overlay article-owned prose without copying or changing executable examples. */
4
+ export function demoCopy(frontmatter, locale, key, original) {
5
+ if (locale === 'en') return original
6
+ const copy = frontmatter.demos?.[key]
7
+ const walk = (value, translated, field, location) => {
8
+ if (typeof value === 'string') {
9
+ if (!proseFields.has(field) || !value.trim()) return value
10
+ if (typeof translated !== 'string' || !translated.trim()) throw new Error(`Missing localized demo copy: ${key}.${location} (${locale})`)
11
+ return translated
12
+ }
13
+ if (Array.isArray(value)) return value.map((item, index) => walk(item, translated?.[index], field, `${location}[${index}]`))
14
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, walk(item, translated?.[name], name, location ? `${location}.${name}` : name)]))
15
+ return value
16
+ }
17
+ return walk(original, copy, '', '')
18
+ }
@@ -2,6 +2,7 @@ import { defu } from 'defu'
2
2
  import type { Metadata } from '../types/Metadata'
3
3
  import type { Props } from '../types/Props'
4
4
  import { createTranslation } from './i18n'
5
+ import { resolveMetadata } from '../factories/create-page'
5
6
 
6
7
  export default async function generate(
7
8
  metadata: Metadata | Record<string, any>,
@@ -9,6 +10,7 @@ export default async function generate(
9
10
  dictionaries: Record<string, () => Promise<any>>
10
11
  ): Promise<Metadata> {
11
12
  const { locale } = params
13
+ metadata = resolveMetadata(locale, metadata)
12
14
  const $ = await createTranslation(locale, dictionaries)
13
15
  const description = $(metadata.description)
14
16
  const category = $(metadata.category)
@@ -38,7 +40,9 @@ export default async function generate(
38
40
  card: 'summary_large_image'
39
41
  }
40
42
  }
41
- if (metadata.openGraph?.images) {
43
+ if (metadata.cover) {
44
+ baseMetadata.openGraph.images = baseMetadata.twitter.images = metadata.cover.socialImage ?? metadata.cover.src
45
+ } else if (metadata.openGraph?.images) {
42
46
  baseMetadata.twitter.images = metadata.openGraph.images
43
47
  } else {
44
48
  // postbuild-static renders each placeholder into a static /og image.
@@ -57,16 +57,19 @@ export function resolveHead(metadata: Partial<Metadata>, { project, siteURL, pat
57
57
  }
58
58
  }
59
59
  const { openGraph, twitter } = metadata
60
+ const coverImage = metadata.cover?.socialImage ?? metadata.cover?.src
60
61
  meta('property', 'og:title', openGraph?.title)
61
62
  meta('property', 'og:description', openGraph?.description)
62
63
  meta('property', 'og:site_name', openGraph?.siteName)
63
64
  // Open Graph and Twitter cards require absolute image URLs.
64
- for (const image of list(openGraph?.images)) meta('property', 'og:image', absolute(image))
65
+ for (const image of list(coverImage ?? openGraph?.images)) meta('property', 'og:image', absolute(image))
66
+ if (metadata.cover) meta('property', 'og:image:alt', metadata.cover.alt)
65
67
  meta('name', 'twitter:card', twitter?.card)
66
68
  meta('name', 'twitter:site', twitter?.site)
67
69
  meta('name', 'twitter:creator', twitter?.creator)
68
70
  meta('name', 'twitter:title', twitter?.title)
69
71
  meta('name', 'twitter:description', twitter?.description)
70
- for (const image of list(twitter?.images)) meta('name', 'twitter:image', absolute(image))
72
+ for (const image of list(coverImage ?? twitter?.images)) meta('name', 'twitter:image', absolute(image))
73
+ if (metadata.cover) meta('name', 'twitter:image:alt', metadata.cover.alt)
71
74
  return { title: resolveTitle(metadata.title, project), tags }
72
75
  }
@@ -6,6 +6,7 @@ export interface LocalizePathnameOptions {
6
6
  locales: readonly string[]
7
7
  localePrefixMode?: LocalePrefixMode
8
8
  localizablePathnameRoots?: readonly string[]
9
+ localizableOrigins?: Record<string, readonly string[]>
9
10
  }
10
11
 
11
12
  const nonPagePathnameRoots = new Set([
@@ -24,8 +25,18 @@ export function localizePathname(href: string, {
24
25
  locale,
25
26
  locales,
26
27
  localePrefixMode = 'canonical',
27
- localizablePathnameRoots
28
- }: LocalizePathnameOptions) {
28
+ localizablePathnameRoots,
29
+ localizableOrigins
30
+ }: LocalizePathnameOptions): string {
31
+ if (/^https?:\/\//i.test(href)) {
32
+ let url: URL
33
+ try { url = new URL(href) } catch { return href }
34
+ const roots = localizableOrigins?.[url.origin]
35
+ if (!roots) return href
36
+ return url.origin + localizePathname(url.pathname + url.search + url.hash, {
37
+ defaultLocale, locale, locales, localePrefixMode, localizablePathnameRoots: roots
38
+ })
39
+ }
29
40
  if (!href || !href.startsWith('/') || href.startsWith('//')) return href
30
41
 
31
42
  const parsedHref = parseLocalHref(href)
@@ -1,3 +1,5 @@
1
+ import sharedTranslations from '../common/translations'
2
+
1
3
  export async function createTranslation(locale: string, dictionaries: Record<string, () => Promise<any>>): Promise<(text: any) => string> {
2
4
  const translations = await importTranslations(locale, dictionaries)
3
5
  return (text: any) => (translations)[text || ''] || text
@@ -8,5 +10,6 @@ export const importTranslations = async (locale: string, dictionaries: Record<st
8
10
  throw new Error('locale is required')
9
11
  }
10
12
  const load = dictionaries[locale as keyof typeof dictionaries]
11
- return await load()
13
+ if (!load && !sharedTranslations[locale]) throw new Error(`Unsupported locale: ${locale}`)
14
+ return { ...sharedTranslations[locale], ...(load ? await load() : {}) }
12
15
  }
@@ -0,0 +1,22 @@
1
+ /** Prose can vary by locale; publication identity and technical configuration stay with the source. */
2
+ export function localizedMetadata(metadata: Record<string, any>, frontmatter: Record<string, any>, locale: string) {
3
+ const localized: Record<string, any> = { ...metadata, locale }
4
+ for (const key of ['description', 'subtitle']) if (typeof frontmatter[key] === 'string') localized[key] = frontmatter[key]
5
+ if (typeof frontmatter.title === 'string') {
6
+ localized.title = metadata.title && typeof metadata.title === 'object'
7
+ ? { ...metadata.title, ...('absolute' in metadata.title ? { absolute: frontmatter.title } : { default: frontmatter.title }) }
8
+ : frontmatter.title
9
+ }
10
+ if (metadata.cover && typeof frontmatter.cover?.alt === 'string') localized.cover = { ...metadata.cover, alt: frontmatter.cover.alt }
11
+ for (const channel of ['openGraph', 'twitter']) {
12
+ const authored = frontmatter[channel]
13
+ if (metadata[channel] || authored) {
14
+ const prose = Object.fromEntries(['title', 'description'].flatMap(key => {
15
+ const text = authored?.[key] ?? (locale !== 'en' ? frontmatter[key] : undefined)
16
+ return typeof text === 'string' ? [[key, text]] : []
17
+ }))
18
+ localized[channel] = { ...metadata[channel], ...prose }
19
+ }
20
+ }
21
+ return localized
22
+ }
@@ -0,0 +1,61 @@
1
+ import { markdownTree } from './markdown-tree'
2
+
3
+ interface LinkNode {
4
+ type: string
5
+ url?: string
6
+ children?: LinkNode[]
7
+ position?: { start: { offset: number }; end: { offset: number } }
8
+ }
9
+
10
+ function destination(source: string, node: LinkNode): { start: number; end: number; angled: boolean } {
11
+ const { start, end } = node.position!
12
+ let at = start.offset
13
+ if (node.type === 'definition') {
14
+ for (; at < end.offset; at++) {
15
+ if (source[at] === '\\') { at++; continue }
16
+ if (source[at] === ']' && source[at + 1] === ':') { at += 2; break }
17
+ }
18
+ } else if (source[at] === '[') {
19
+ // Child positions include inline-code and nested formatting in the label.
20
+ at = node.children?.at(-1)?.position?.end.offset ?? at + 1
21
+ at = source.indexOf('](', at) + 2
22
+ if (at < 2) throw new Error(`Missing Markdown link destination: ${node.url}`)
23
+ } else {
24
+ // GFM bare links and CommonMark angle autolinks have no bracketed label.
25
+ const angled = source[at] === '<'
26
+ return { start: at + Number(angled), end: end.offset - Number(angled), angled }
27
+ }
28
+ while (/\s/.test(source[at] ?? '') && at < end.offset) at++
29
+ const angled = source[at] === '<'
30
+ const begin = at + Number(angled)
31
+ at = begin
32
+ let depth = 0
33
+ for (; at < end.offset; at++) {
34
+ const character = source[at]
35
+ if (character === '\\') { at++; continue }
36
+ if (angled ? character === '>' : /\s/.test(character) || (character === ')' && depth === 0)) break
37
+ if (!angled && character === '(') depth++
38
+ else if (!angled && character === ')') depth--
39
+ }
40
+ return { start: begin, end: at, angled }
41
+ }
42
+
43
+ /** Edit actual Markdown link/definition URLs without reserializing prose or code. */
44
+ export function transformMarkdownLinks(markdown: string, mapper: (url: string) => string): string {
45
+ const edits: { start: number; end: number; value: string }[] = []
46
+ function visit(node: LinkNode) {
47
+ if ((node.type === 'link' || node.type === 'definition') && node.position && typeof node.url === 'string') {
48
+ const mapped = mapper(node.url)
49
+ if (mapped !== node.url) {
50
+ const range = destination(markdown, node)
51
+ const escaped = mapped.replace(/([\\<>])/g, '\\$1')
52
+ const value = range.angled ? escaped : /\s/.test(mapped) ? `<${escaped}>` : escaped.replace(/([()])/g, '\\$1')
53
+ edits.push({ ...range, value })
54
+ }
55
+ }
56
+ node.children?.forEach(visit)
57
+ }
58
+ visit(markdownTree(markdown) as LinkNode)
59
+ for (const edit of edits.sort((a, b) => b.start - a.start)) markdown = markdown.slice(0, edit.start) + edit.value + markdown.slice(edit.end)
60
+ return markdown
61
+ }
@@ -1,19 +1,32 @@
1
1
  import { remark } from 'remark'
2
2
  import gfm from 'remark-gfm'
3
- import Slugger from 'github-slugger'
4
- import resolveHeading from './resolve-heading'
3
+ import resolveHeading, { createHeadingResolver } from './resolve-heading'
5
4
 
6
5
  const parser = remark().use(gfm)
7
- export const markdownTree = (source: string) => parser.parse(source)
6
+ export function markdownTree(source: string) {
7
+ const tree = parser.parse(source)
8
+ const resolveHeading = createHeadingResolver()
9
+ function visit(node: any) {
10
+ if (node.type === 'heading' && node.depth >= 2 && node.depth <= 6) {
11
+ const title = node.children.filter((child: any) => child.type === 'text' || child.type === 'inlineCode').map((child: any) => child.value).join(' ').trim()
12
+ const resolved = resolveHeading(title, node.depth)
13
+ node.data ??= {}
14
+ node.data.id = resolved.id
15
+ node.data.hProperties = { ...node.data.hProperties, id: resolved.id }
16
+ }
17
+ node.children?.forEach(visit)
18
+ }
19
+ visit(tree)
20
+ return tree
21
+ }
8
22
 
9
23
  export function markdownHeadings(source: string) {
10
- const slugger = new Slugger()
11
24
  const headings: { id: string; title: string; depth: number }[] = []
12
25
  function visit(node: any) {
13
26
  if (node.type === 'heading' && (node.depth === 2 || node.depth === 3)) {
14
27
  const title = node.children.filter((child: any) => child.type === 'text' || child.type === 'inlineCode').map((child: any) => child.value).join(' ').trim()
15
- const resolved = resolveHeading(title, slugger.slug(title))
16
- headings.push({ id: resolved.id!, title: resolved.title, depth: node.depth })
28
+ const resolved = resolveHeading(title, node.data.hProperties.id)
29
+ headings.push({ id: node.data.hProperties.id, title: resolved.title, depth: node.depth })
17
30
  }
18
31
  node.children?.forEach(visit)
19
32
  }
@@ -1 +1,2 @@
1
1
  export default function resolveHeading(title: string, id?: string): { title: string; id?: string; className?: string }
2
+ export function createHeadingResolver(): (title: string, depth: number) => { title: string; id?: string; className?: string; explicit: boolean }
@@ -1,3 +1,5 @@
1
+ import Slugger from 'github-slugger'
2
+
1
3
  export default function resolveHeading(title, id) {
2
4
  const explicit = typeof title === 'string' && title.match(/\s+\{#([\w-]+)\}$/)
3
5
  if (explicit) {
@@ -17,3 +19,22 @@ export default function resolveHeading(title, id) {
17
19
  }
18
20
  return { title, id }
19
21
  }
22
+
23
+ /** Section counters stay stable when a detail heading is inserted between them. */
24
+ export function createHeadingResolver() {
25
+ const sections = new Slugger()
26
+ const details = new Slugger()
27
+ const assigned = new Set()
28
+ return (title, depth) => {
29
+ const explicit = /\s+\{#[\w-]+\}$/.test(title)
30
+ let heading
31
+ if (explicit) heading = resolveHeading(title)
32
+ else {
33
+ const slugs = depth === 2 || depth === 3 ? sections : details
34
+ do { heading = resolveHeading(title, slugs.slug(title)) }
35
+ while (heading.id && assigned.has(heading.id))
36
+ }
37
+ if (heading.id) assigned.add(heading.id)
38
+ return { ...heading, explicit }
39
+ }
40
+ }
@@ -1,15 +1,29 @@
1
1
  import FastGlob from 'fast-glob'
2
- import Slugger from 'github-slugger'
3
2
  import { existsSync, mkdirSync, readFileSync } from 'node:fs'
4
3
  import { dirname, join, resolve } from 'node:path'
5
4
  import { pathToFileURL } from 'node:url'
6
- import { fromMarkdown } from 'mdast-util-from-markdown'
5
+ import { remark } from 'remark'
6
+ import gfm from 'remark-gfm'
7
7
  import { mdxjsEsm } from 'micromark-extension-mdxjs-esm'
8
+ import { mdxJsx } from 'micromark-extension-mdx-jsx'
9
+ import { mdxJsxFromMarkdown } from 'mdast-util-mdx-jsx'
8
10
  import { mdxjsEsmFromMarkdown } from 'mdast-util-mdxjs-esm'
9
11
  import * as acorn from 'acorn'
10
- import resolveHeading from './resolve-heading.js'
12
+ import { createHeadingResolver } from './resolve-heading.js'
11
13
  import { writeIfChangedSync } from '../../scripts/write-if-changed'
12
14
  import { localizePathname, type LocalePrefixMode } from './i18n-pathname.js'
15
+ import { parseFrontmatter } from '@astrojs/markdown-remark'
16
+ import { localizedMetadata } from './localized-metadata'
17
+
18
+ // Parse complete MDX boundaries so imports in example props are not mistaken for article ESM.
19
+ const markdownParser = remark().use(gfm)
20
+ const searchParser = remark().use(gfm).use(function mdxSearchSyntax() {
21
+ const data = this.data()
22
+ ;(data.micromarkExtensions ??= []).push({ disable: { null: ['htmlFlow', 'htmlText', 'codeIndented'] } }, mdxjsEsm({ acorn, addResult: true }), mdxJsx({ acorn, addResult: true }))
23
+ // Consumers may resolve another mdast type version through remark-parse.
24
+ const extensions = [mdxjsEsmFromMarkdown(), mdxJsxFromMarkdown()]
25
+ ;(data.fromMarkdownExtensions ??= []).push(...extensions as unknown as NonNullable<typeof data.fromMarkdownExtensions>)
26
+ })
13
27
 
14
28
  const DEFAULT_METADATA_GLOB = './content/**/*metadata.ts'
15
29
 
@@ -63,6 +77,7 @@ interface MdxNode {
63
77
  depth?: number
64
78
  type: string
65
79
  value?: string
80
+ explicitId?: string
66
81
  }
67
82
 
68
83
  export async function generateSearchPages({
@@ -82,6 +97,12 @@ export async function generateSearchPages({
82
97
  const translations = loadDictionaryTranslations(dictionariesDir, locale)
83
98
  const translate = createDictionaryTranslate(translations)
84
99
  const pages = entries
100
+ .filter(entry => {
101
+ if (entry.metadata.draft) return false
102
+ const content = locale === defaultLocale ? entry.content : entry.localizedContent?.[locale]
103
+ if (locale !== defaultLocale && entry.content && !content) return false
104
+ return parseFrontmatter(content ?? '').frontmatter.draft !== true
105
+ })
85
106
  .map((entry) => createSearchPage({ defaultLocale, entry, locale, translate }))
86
107
  .sort((a, b) => a.url.localeCompare(b.url))
87
108
  result[locale] = pages
@@ -102,7 +123,7 @@ export async function loadSearchPageEntries({
102
123
  .filter((metadataPath) => (existsSync(resolve(cwd, dirname(metadataPath), 'page.astro')) || existsSync(resolve(cwd, dirname(metadataPath), 'content.mdx'))))
103
124
  .sort()
104
125
 
105
- return await Promise.all(metadataPaths.map(async (metadataPath) => {
126
+ return (await Promise.all(metadataPaths.map(async (metadataPath) => {
106
127
  const metadataModule = await import(pathToFileURL(resolve(cwd, metadataPath)).href)
107
128
  const contentPath = resolve(cwd, dirname(metadataPath), 'content.mdx')
108
129
  const contentDir = resolve(cwd, dirname(metadataPath))
@@ -112,7 +133,7 @@ export async function loadSearchPageEntries({
112
133
  metadata: metadataModule.default,
113
134
  metadataPath
114
135
  }
115
- }))
136
+ }))).filter(entry => !entry.metadata.draft && !parseFrontmatter(entry.content ?? '').frontmatter.draft)
116
137
  }
117
138
 
118
139
  export function createSearchPage({
@@ -121,11 +142,15 @@ export function createSearchPage({
121
142
  locale,
122
143
  translate = (text) => text
123
144
  }: CreateSearchPageOptions): SearchPage {
124
- const { metadata } = entry
145
+ const source = locale === defaultLocale ? entry.content : entry.localizedContent?.[locale]
146
+ if (locale !== defaultLocale && entry.content && !source) throw new Error(`Missing search translation: ${entry.metadata.pathname} (${locale})`)
147
+ const parsed = source === undefined ? undefined : parseFrontmatter(source)
148
+ if (parsed?.frontmatter.draft) throw new Error(`Draft translation cannot be indexed: ${entry.metadata.pathname} (${locale})`)
149
+ const metadata = localizedMetadata(entry.metadata, parsed?.frontmatter ?? {}, locale)
125
150
  const title = translate(resolveMetadataString(metadata.title))
126
151
  const description = translate(resolveMetadataString(metadata.description))
127
152
  const category = translate(resolveMetadataString(metadata.category))
128
- const content = entry.localizedContent?.[locale] ?? entry.content
153
+ const content = parsed?.content
129
154
  const page: SearchPage = {
130
155
  category,
131
156
  description,
@@ -170,26 +195,50 @@ export function createLocalizedPathname(pathname: string, locale: string, defaul
170
195
  }
171
196
 
172
197
  export function extractSearchNodesFromMdx(source: string): SearchNode[] {
173
- const tree = fromMarkdown(source, {
174
- extensions: [mdxjsEsm({ acorn, addResult: true })],
175
- mdastExtensions: [mdxjsEsmFromMarkdown()]
176
- }) as MdxNode
198
+ const tree = searchParser.parse(parseFrontmatter(source).content) as MdxNode
199
+ return searchNodesFromTree(tree)
200
+ }
201
+
202
+ /** Portable exports are Markdown and may include native HTML comments or type notation. */
203
+ export function extractSearchNodesFromMarkdown(source: string): SearchNode[] {
204
+ const tree = markdownParser.parse(parseFrontmatter(source).content) as MdxNode
205
+ function resolvePortableAnchors(parent: MdxNode) {
206
+ let pendingId: string | undefined
207
+ for (const node of parent.children ?? []) {
208
+ const markup = node.type === 'html' ? node.value : node.type === 'paragraph'
209
+ && node.children?.every(child => child.type === 'html' || (child.type === 'text' && !child.value?.trim()))
210
+ ? node.children.map(child => child.value ?? '').join('') : undefined
211
+ const anchors = [...(markup ?? '').matchAll(/<a\s+id="([^"]+)"\s*><\/a>/g)]
212
+ if (anchors.length && !markup!.replace(/<a\s+id="([^"]+)"\s*><\/a>/g, '').trim()) {
213
+ pendingId = anchors.at(-1)![1]
214
+ continue
215
+ }
216
+ if (node.type === 'heading' && pendingId) node.explicitId = pendingId
217
+ pendingId = undefined
218
+ if (node.children) resolvePortableAnchors(node)
219
+ }
220
+ }
221
+ resolvePortableAnchors(tree)
222
+ return searchNodesFromTree(tree)
223
+ }
224
+
225
+ function searchNodesFromTree(tree: MdxNode): SearchNode[] {
177
226
  const nodes: SearchNode[] = []
178
- const slugger = new Slugger()
179
- appendSearchNodes(tree.children ?? [], nodes, slugger)
227
+ const resolveHeading = createHeadingResolver()
228
+ appendSearchNodes(tree.children ?? [], nodes, resolveHeading)
180
229
  return nodes
181
230
  }
182
231
 
183
- function appendSearchNodes(children: MdxNode[], nodes: SearchNode[], slugger: Slugger) {
232
+ function appendSearchNodes(children: MdxNode[], nodes: SearchNode[], resolveHeading: ReturnType<typeof createHeadingResolver>) {
184
233
  for (const child of children) {
185
- appendSearchNode(child, nodes, slugger)
234
+ appendSearchNode(child, nodes, resolveHeading)
186
235
  }
187
236
  }
188
237
 
189
- function appendSearchNode(node: MdxNode, nodes: SearchNode[], slugger: Slugger) {
238
+ function appendSearchNode(node: MdxNode, nodes: SearchNode[], resolveHeading: ReturnType<typeof createHeadingResolver>) {
190
239
  switch (node.type) {
191
240
  case 'heading':
192
- appendHeadingSearchNode(node, nodes, slugger)
241
+ appendHeadingSearchNode(node, nodes, resolveHeading)
193
242
  return
194
243
  case 'paragraph':
195
244
  appendTextSearchNode(nodes, collectText(node), 'p')
@@ -202,8 +251,11 @@ function appendSearchNode(node: MdxNode, nodes: SearchNode[], slugger: Slugger)
202
251
  appendTextSearchNode(nodes, collectText(child), 'li')
203
252
  }
204
253
  return
254
+ case 'table':
255
+ for (const row of node.children ?? []) appendTextSearchNode(nodes, (row.children ?? []).map(collectText).join(' | '), 'tr')
256
+ return
205
257
  case 'blockquote':
206
- appendSearchNodes(node.children ?? [], nodes, slugger)
258
+ appendSearchNodes(node.children ?? [], nodes, resolveHeading)
207
259
  return
208
260
  case 'mdxjsEsm':
209
261
  case 'html':
@@ -212,18 +264,19 @@ function appendSearchNode(node: MdxNode, nodes: SearchNode[], slugger: Slugger)
212
264
  return
213
265
  default:
214
266
  if (node.children?.length) {
215
- appendSearchNodes(node.children, nodes, slugger)
267
+ appendSearchNodes(node.children, nodes, resolveHeading)
216
268
  }
217
269
  }
218
270
  }
219
271
 
220
- function appendHeadingSearchNode(node: MdxNode, nodes: SearchNode[], slugger: Slugger) {
272
+ function appendHeadingSearchNode(node: MdxNode, nodes: SearchNode[], resolveHeading: ReturnType<typeof createHeadingResolver>) {
221
273
  const text = normalizeSearchText((node.children ?? []).filter(child => child.type === 'text' || child.type === 'inlineCode').map(child => child.value).join(' '))
222
274
  if (!text) return
223
275
 
224
276
  const tag = node.depth ? `h${node.depth}` : 'h'
225
- if (node.depth === 2 || node.depth === 3) {
226
- const resolved = resolveHeading(text, slugger.slug(text))
277
+ if (node.depth && node.depth >= 2 && node.depth <= 6) {
278
+ const title = node.explicitId && !/\{#[\w-]+\}/.test(text) ? `${text} {#${node.explicitId}}` : text
279
+ const resolved = resolveHeading(title, node.depth)
227
280
  appendTextSearchNode(nodes, resolved.title, tag, resolved.id)
228
281
  return
229
282
  }
@@ -1,4 +1,8 @@
1
1
  <script lang="ts">
2
+ import '@master/internal-site/styles/system/button-feedback.css'
3
+ import '@master/internal-site/styles/system/buttons.css'
4
+ import '@master/internal-site/styles/system/sheet.css'
5
+
2
6
  import Image from '@master/internal-site/components/Image.svelte'
3
7
  import IconArrowLeft from '@tabler/icons-svelte-runes/icons/arrow-left'
4
8
  import IconArrowUpRight from '@tabler/icons-svelte-runes/icons/arrow-up-right'
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  // Bare examples import the system parts they render: sheets and specimens.
3
- import '@master/internal-site/styles/system/surfaces.css'
4
3
  import '@master/internal-site/styles/system/specimens.css'
5
4
  import DemoItem from '@master/internal-site/demo/DemoItem.svelte'
6
5
  import DemoLabel from '@master/internal-site/demo/DemoLabel.svelte'
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import './bento.css'
2
3
  import type { HTMLAttributes } from 'astro/types'
3
4
 
4
5
  type Props = HTMLAttributes<'div'>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import './bento.css'
2
3
  import type { HTMLAttributes } from 'astro/types'
3
4
 
4
5
  type Props = HTMLAttributes<'div'>