@janga/norna 0.7.24 → 0.7.26

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 (142) hide show
  1. package/README.md +5 -4
  2. package/astro.config.mjs +27 -11
  3. package/bin/norna-cli.mjs +12 -0
  4. package/package.json +26 -5
  5. package/schemas/card-list.schema.json +121 -0
  6. package/schemas/category.schema.json +2 -2
  7. package/schemas/config.schema.json +342 -33
  8. package/schemas/content-frontmatter.schema.json +7 -7
  9. package/schemas/image-carousel.schema.json +43 -0
  10. package/schemas/image-stack.schema.json +43 -0
  11. package/schemas/manifest.json +6 -1
  12. package/schemas/page-theme.schema.json +26 -26
  13. package/schemas/sitewide-content.schema.json +18 -18
  14. package/schemas/theme.schema.json +237 -262
  15. package/scripts/check-config.mjs +5 -1
  16. package/scripts/dev-local.mjs +130 -13
  17. package/scripts/generate-schemas.mjs +15 -3
  18. package/scripts/init-site.mjs +2 -0
  19. package/scripts/lib/category-destinations.mjs +48 -0
  20. package/scripts/lib/code-fence-metadata.mjs +25 -43
  21. package/scripts/lib/content-tabs.mjs +155 -0
  22. package/scripts/lib/details-headings.mjs +58 -0
  23. package/scripts/lib/docusaurus-migration-audit.mjs +603 -0
  24. package/scripts/lib/edit-source-link.mjs +53 -0
  25. package/scripts/lib/editor-language-service.mjs +238 -116
  26. package/scripts/lib/locale-registry.mjs +597 -0
  27. package/scripts/lib/locales/az-Latn.mjs +78 -0
  28. package/scripts/lib/locales/be.mjs +78 -0
  29. package/scripts/lib/locales/bg.mjs +78 -0
  30. package/scripts/lib/locales/bs.mjs +78 -0
  31. package/scripts/lib/locales/ca.mjs +78 -0
  32. package/scripts/lib/locales/cnr.mjs +78 -0
  33. package/scripts/lib/locales/cs.mjs +78 -0
  34. package/scripts/lib/locales/da.mjs +78 -0
  35. package/scripts/lib/locales/de.mjs +78 -0
  36. package/scripts/lib/locales/el.mjs +78 -0
  37. package/scripts/lib/locales/en.mjs +78 -0
  38. package/scripts/lib/locales/es.mjs +78 -0
  39. package/scripts/lib/locales/et.mjs +78 -0
  40. package/scripts/lib/locales/fi.mjs +78 -0
  41. package/scripts/lib/locales/fil.mjs +78 -0
  42. package/scripts/lib/locales/fr.mjs +78 -0
  43. package/scripts/lib/locales/ga.mjs +78 -0
  44. package/scripts/lib/locales/ha.mjs +78 -0
  45. package/scripts/lib/locales/hr.mjs +78 -0
  46. package/scripts/lib/locales/hu.mjs +78 -0
  47. package/scripts/lib/locales/id.mjs +78 -0
  48. package/scripts/lib/locales/is.mjs +78 -0
  49. package/scripts/lib/locales/it.mjs +78 -0
  50. package/scripts/lib/locales/jv.mjs +78 -0
  51. package/scripts/lib/locales/lb.mjs +78 -0
  52. package/scripts/lib/locales/lt.mjs +78 -0
  53. package/scripts/lib/locales/lv.mjs +78 -0
  54. package/scripts/lib/locales/mk.mjs +78 -0
  55. package/scripts/lib/locales/mt.mjs +78 -0
  56. package/scripts/lib/locales/nb.mjs +78 -0
  57. package/scripts/lib/locales/nl.mjs +78 -0
  58. package/scripts/lib/locales/nn.mjs +78 -0
  59. package/scripts/lib/locales/pcm.mjs +78 -0
  60. package/scripts/lib/locales/pl.mjs +78 -0
  61. package/scripts/lib/locales/pt.mjs +78 -0
  62. package/scripts/lib/locales/ro.mjs +78 -0
  63. package/scripts/lib/locales/ru.mjs +78 -0
  64. package/scripts/lib/locales/sk.mjs +78 -0
  65. package/scripts/lib/locales/sl.mjs +78 -0
  66. package/scripts/lib/locales/sq.mjs +78 -0
  67. package/scripts/lib/locales/sr-Cyrl.mjs +78 -0
  68. package/scripts/lib/locales/sr-Latn.mjs +78 -0
  69. package/scripts/lib/locales/sv.mjs +78 -0
  70. package/scripts/lib/locales/sw.mjs +78 -0
  71. package/scripts/lib/locales/tr.mjs +78 -0
  72. package/scripts/lib/locales/uk.mjs +78 -0
  73. package/scripts/lib/locales/vi.mjs +78 -0
  74. package/scripts/lib/markdown-links.mjs +4 -25
  75. package/scripts/lib/markdown-notes-render-plugin.mjs +75 -0
  76. package/scripts/lib/markdown-notes.mjs +165 -0
  77. package/scripts/lib/navigation-model.mjs +1 -1
  78. package/scripts/lib/norna-markdown-blocks.mjs +123 -447
  79. package/scripts/lib/norna-markdown-render-plugin.mjs +4 -0
  80. package/scripts/lib/page-aliases.mjs +12 -1
  81. package/scripts/lib/page-markdown.mjs +63 -16
  82. package/scripts/lib/page-move-plan.mjs +22 -5
  83. package/scripts/lib/pagefind-translations.mjs +620 -0
  84. package/scripts/lib/presentation-contract.mjs +6 -1
  85. package/scripts/lib/presentation.mjs +29 -30
  86. package/scripts/lib/project-config.mjs +22 -163
  87. package/scripts/lib/schema-definitions.mjs +13 -12
  88. package/scripts/lib/schema-editor-metadata.mjs +42 -46
  89. package/scripts/lib/schema-value-definitions.mjs +7 -8
  90. package/scripts/lib/semantic-callouts.mjs +4 -2
  91. package/scripts/lib/site-content.mjs +4 -2
  92. package/scripts/lib/site-link-graph.mjs +16 -6
  93. package/scripts/lib/site-navigation-tree.mjs +0 -11
  94. package/scripts/lib/site-paths.mjs +14 -2
  95. package/scripts/lib/sitemap.mjs +5 -3
  96. package/scripts/lib/structured-blocks.mjs +108 -0
  97. package/scripts/lib/table-render-plugin.mjs +76 -0
  98. package/scripts/lib/table-row-headers.mjs +218 -0
  99. package/scripts/lib/theme-presets.mjs +3 -10
  100. package/scripts/lib/yaml-config.mjs +2 -6
  101. package/scripts/migrate-check.mjs +19 -0
  102. package/scripts/sync-content-sections.mjs +39 -10
  103. package/scripts/sync-site-public.mjs +7 -1
  104. package/src/components/CardList.astro +1 -0
  105. package/src/components/CodeBlockCopyScript.astro +49 -6
  106. package/src/components/ContentBlocks.astro +39 -0
  107. package/src/components/ContentTabsScript.astro +60 -0
  108. package/src/components/DisplaySettings.astro +17 -19
  109. package/src/components/ImageCarousel.astro +4 -1
  110. package/src/components/ImageStack.astro +34 -9
  111. package/src/components/ImageStackEnhancement.astro +331 -0
  112. package/src/components/NavigationPageTree.astro +1 -1
  113. package/src/components/NoteNavigationScript.astro +4 -0
  114. package/src/components/PageAliasRedirect.astro +5 -4
  115. package/src/components/PageContentsNavigation.astro +2 -3
  116. package/src/components/SearchNavigationScript.astro +4 -0
  117. package/src/components/SearchPage.astro +43 -2
  118. package/src/components/SectionNavigationScript.astro +86 -16
  119. package/src/components/SiteBreadcrumbs.astro +1 -1
  120. package/src/components/SiteNavigation.astro +98 -65
  121. package/src/components/SitePage.astro +73 -33
  122. package/src/components/SiteSection.astro +5 -81
  123. package/src/components/SiteTreeNavigation.astro +5 -2
  124. package/src/components/TableOverflowScript.astro +579 -0
  125. package/src/components/TopPageMenu.astro +47 -0
  126. package/src/components/TreeNavigationScript.astro +86 -23
  127. package/src/layouts/BaseLayout.astro +34 -12
  128. package/src/lib/generatedImages.ts +18 -0
  129. package/src/lib/navigationFollowing.ts +142 -0
  130. package/src/lib/noteNavigation.ts +50 -0
  131. package/src/lib/searchNavigation.ts +107 -0
  132. package/src/lib/sectionContent.ts +23 -116
  133. package/src/lib/siteNavigation.ts +0 -5
  134. package/src/lib/sitePages.ts +18 -4
  135. package/src/lib/table-sort-tooltip.ts +109 -0
  136. package/src/pages/[...slug].astro +16 -6
  137. package/src/styles/content.css +563 -26
  138. package/src/styles/foundations.css +9 -0
  139. package/src/styles/media.css +192 -3
  140. package/src/styles/navigation.css +214 -60
  141. package/src/styles/page-layout.css +55 -16
  142. package/src/styles/responsive.css +62 -20
@@ -1,19 +1,20 @@
1
1
  ---
2
- import { Search } from '@lucide/astro';
2
+ import { Menu, Search, X } from '@lucide/astro';
3
3
  import projectConfig from '../../scripts/lib/project-config.mjs';
4
4
  import { withBasePath } from '../lib/basePath';
5
5
  import type { SectionNavigation } from '../lib/sectionContent';
6
6
  import {
7
- getFirstPageInNavigationNode,
8
7
  getListedSiteNavigationTree,
9
8
  type SiteNavigationEntry,
10
9
  type SiteNavigationNode,
11
10
  } from '../lib/siteNavigation';
12
- import type { SiteNode, SitePage } from '../lib/sitePages';
11
+ import type { SiteNode } from '../lib/sitePages';
13
12
  import DisplaySettings from './DisplaySettings.astro';
14
13
  import NavigationPageTree from './NavigationPageTree.astro';
15
14
  import NavigationTreeControls from './NavigationTreeControls.astro';
15
+ import TopPageMenu from './TopPageMenu.astro';
16
16
  import SectionNavigationScript from './SectionNavigationScript.astro';
17
+ import SearchNavigationScript from './SearchNavigationScript.astro';
17
18
 
18
19
  type FrameColors = {
19
20
  backgroundColor: string;
@@ -26,7 +27,7 @@ interface Props {
26
27
  alt: string;
27
28
  height?: string;
28
29
  };
29
- currentPage?: SitePage;
30
+ currentPage?: SiteNode;
30
31
  navigationEntries: SiteNavigationEntry[];
31
32
  navigationMode: 'sections' | 'top' | 'tree';
32
33
  showTopNavigationSubmenus: boolean;
@@ -67,6 +68,12 @@ const navigationStyle = [
67
68
  ...(logo?.height ? [`--site-brand-logo-height: ${logo.height}`] : []),
68
69
  ].join('; ');
69
70
  const siteNavigationTree = getListedSiteNavigationTree(navigationEntries);
71
+ const mobileNavigationTree = navigationMode === 'top'
72
+ ? getListedSiteNavigationTree(navigationEntries.map((entry) => ({
73
+ ...entry,
74
+ headings: entry.headings.filter((heading) => heading.depth === 2),
75
+ })))
76
+ : siteNavigationTree;
70
77
  const topLevelNavigation = siteNavigationTree;
71
78
  const showSiteNavigation = topLevelNavigation.some(({ node }) => !node.isHome);
72
79
  const homeNode = navigationEntries.find(({ node }) => node.isHome)?.node;
@@ -75,13 +82,13 @@ if (!homePage) throw new Error('Site navigation requires a homepage.');
75
82
  const currentPageNavigation = currentPage
76
83
  ? navigationEntries.find(({ node }) => node.kind === 'page' && node.pathname === currentPage.pathname)
77
84
  : undefined;
78
- const isCurrentPage = (page: SitePage) => currentPage?.pathname === page.pathname;
85
+ const isCurrentPage = (page: SiteNode) => currentPage?.pathname === page.pathname;
79
86
  const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
80
87
  currentPage.pageDirectory === node.pageDirectory
81
88
  || currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
82
89
  ));
83
- const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
84
- const getNavigationPage = (node: SiteNavigationNode) => getFirstPageInNavigationNode(node);
90
+ const pageHref = (page: SiteNode) => withBasePath(projectConfig.site.basePath, page.pathname);
91
+ const getNavigationPage = (node: SiteNavigationNode) => node.node;
85
92
  const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
86
93
  const showSinglePageTopLink = !showSiteNavigation;
87
94
  const currentPageSectionCount = currentPageNavigation?.sections.length ?? 0;
@@ -97,6 +104,7 @@ const hasTreePageContents = Boolean(
97
104
  );
98
105
  const showPageNavigation = Boolean(
99
106
  navigationMode !== 'tree'
107
+ && (navigationMode !== 'top' || showSinglePageTopLink)
100
108
  && currentPageNavigation
101
109
  && (showSinglePageTopLink || hasCurrentPageSectionMenu),
102
110
  );
@@ -107,7 +115,7 @@ const showSearch = projectConfig.search.enabled;
107
115
  const searchHref = withBasePath(projectConfig.site.basePath, '/search/');
108
116
  const showDesktopNavigationRow = showSiteNavigation || Boolean(logo) || showReaderControls || showSearch;
109
117
  const showNavigationRow = showMobileNavigation || showDesktopNavigationRow;
110
- const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents || navigationMode === 'tree';
118
+ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu || hasTreePageContents;
111
119
  ---
112
120
 
113
121
  <a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
@@ -138,22 +146,20 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
138
146
  {topLevelNavigation.map((navigationNode) => {
139
147
  const destination = getNavigationPage(navigationNode);
140
148
  if (!destination) return null;
141
- const nodeIsCurrentPage = navigationNode.node.kind === 'page' && isCurrentPage(navigationNode.node);
149
+ const nodeIsCurrentPage = isCurrentPage(navigationNode.node);
142
150
  return (
143
151
  <li class:list={['site-nav-item', {
144
- 'site-nav-item-has-submenu': showTopNavigationSubmenus && navigationNode.children.length > 0,
145
152
  'site-nav-item-current-branch': !nodeIsCurrentPage && isCurrentBranch(navigationNode.node),
146
153
  }]}>
147
154
  <a href={pageHref(destination)} aria-current={nodeIsCurrentPage ? 'page' : undefined}>
148
155
  {navigationNode.node.title}
149
156
  </a>
150
- {showTopNavigationSubmenus && navigationNode.children.length > 0 && (
151
- <div class="site-nav-submenu">
152
- <nav aria-label={`${navigationNode.node.title} ${projectConfig.locale.labels.siteNavigation}`}>
153
- <NavigationPageTree nodes={[navigationNode]} currentPage={currentPage} variant="submenu" />
154
- </nav>
155
- </div>
156
- )}
157
+ <TopPageMenu
158
+ node={navigationNode}
159
+ currentPage={currentPage}
160
+ showSections={navigationMode === 'top'}
161
+ showChildren={showTopNavigationSubmenus}
162
+ />
157
163
  </li>
158
164
  );
159
165
  })}
@@ -165,6 +171,8 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
165
171
  <a
166
172
  class="site-search-link"
167
173
  href={searchHref}
174
+ data-search-base={projectConfig.site.basePath}
175
+ data-search-source-title={currentPage?.title}
168
176
  aria-current={searchPage ? 'page' : undefined}
169
177
  aria-label={projectConfig.locale.labels.search}
170
178
  title={projectConfig.locale.labels.search}
@@ -177,56 +185,80 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
177
185
  <DisplaySettings controls={readerPreferences.controls} defaults={readerPreferences.defaults} />
178
186
  )}
179
187
 
180
- {showMobileNavigation && <details class="mobile-nav-menu">
181
- <summary>
182
- <span>{projectConfig.locale.labels.navigationMenu}</span>
183
- </summary>
184
- <div class="mobile-nav-panel">
185
- <div class="mobile-site-nav" data-navigation-root={pageHref(homePage)}>
186
- {showSiteNavigation && (
187
- <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
188
- <h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
189
- <NavigationTreeControls
190
- idPrefix="mobile-tree-navigation"
191
- nodes={siteNavigationTree}
192
- />
193
- <NavigationPageTree
194
- nodes={siteNavigationTree}
195
- currentPage={currentPage}
196
- variant="mobile"
197
- integratePageSections={navigationMode === 'tree'}
198
- />
199
- </nav>
200
- )}
188
+ {showMobileNavigation && (
189
+ <details class="mobile-nav-menu" data-compact-navigation>
190
+ <summary>
191
+ <span>{projectConfig.locale.labels.navigationMenu}</span>
192
+ <Menu class="mobile-nav-trigger-icon mobile-nav-trigger-open" aria-hidden="true" />
193
+ <X class="mobile-nav-trigger-icon mobile-nav-trigger-close" aria-hidden="true" />
194
+ </summary>
195
+ <div
196
+ class="mobile-nav-panel"
197
+ data-compact-navigation-panel
198
+ role="dialog"
199
+ aria-modal="true"
200
+ aria-label={projectConfig.locale.labels.navigationMenu}
201
+ >
202
+ <div class="mobile-nav-panel-actions">
203
+ <button
204
+ type="button"
205
+ class="mobile-nav-close"
206
+ data-compact-navigation-close
207
+ aria-label={projectConfig.locale.labels.closeNavigation}
208
+ >
209
+ <X aria-hidden="true" />
210
+ </button>
211
+ </div>
212
+ <div
213
+ class="mobile-site-nav"
214
+ data-navigation-root={pageHref(homePage)}
215
+ data-navigation-state-root={pageHref(homePage)}
216
+ >
217
+ {showSiteNavigation && (
218
+ <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
219
+ <h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
220
+ <NavigationTreeControls
221
+ idPrefix="mobile-tree-navigation"
222
+ nodes={mobileNavigationTree}
223
+ />
224
+ <NavigationPageTree
225
+ nodes={mobileNavigationTree}
226
+ currentPage={currentPage}
227
+ variant="mobile"
228
+ integratePageSections={navigationMode === 'tree' || navigationMode === 'top'}
229
+ />
230
+ </nav>
231
+ )}
201
232
 
202
- {navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
203
- <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
204
- {!showSinglePageTopLink && (
205
- <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
206
- )}
207
- <ul class="mobile-nav-sections">
208
- {showSinglePageTopLink && (
209
- <li class="mobile-nav-page-top-item">
210
- <a
211
- class="mobile-nav-destination mobile-nav-page-top"
212
- data-page-top
213
- href={pageHref(currentPage)}
214
- >
215
- {currentPage.title}
216
- </a>
217
- </li>
218
- )}
219
- {currentPageNavigation.sections.map((section) => (
220
- <li>
221
- <a class="mobile-nav-destination" href={`#${section.id}`}>{section.title}</a>
222
- </li>
223
- ))}
224
- </ul>
225
- </nav>
233
+ {(navigationMode === 'sections' || !showSiteNavigation) && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
234
+ <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
235
+ {!showSinglePageTopLink && (
236
+ <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
237
+ )}
238
+ <ul class="mobile-nav-sections">
239
+ {showSinglePageTopLink && (
240
+ <li class="mobile-nav-page-top-item">
241
+ <a
242
+ class="mobile-nav-destination mobile-nav-page-top"
243
+ data-page-top
244
+ href={pageHref(currentPage)}
245
+ >
246
+ {currentPage.title}
247
+ </a>
248
+ </li>
249
+ )}
250
+ {currentPageNavigation.sections.map((section) => (
251
+ <li>
252
+ <a class="mobile-nav-destination" href={`#${section.id}`}>{section.title}</a>
253
+ </li>
254
+ ))}
255
+ </ul>
256
+ </nav>
257
+ )}
258
+ </div>
259
+ </div>
260
+ </details>
226
261
  )}
227
- </div>
228
- </div>
229
- </details>}
230
262
  </div>
231
263
  </div>
232
264
  )}
@@ -261,3 +293,4 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
261
293
  </header>
262
294
 
263
295
  {needsNavigationScript && <SectionNavigationScript />}
296
+ {showSearch && <SearchNavigationScript />}
@@ -2,7 +2,10 @@
2
2
  import { getCollection, type CollectionEntry } from 'astro:content';
3
3
  import BaseLayout from '../layouts/BaseLayout.astro';
4
4
  import CodeBlockCopyScript from './CodeBlockCopyScript.astro';
5
+ import ContentTabsScript from './ContentTabsScript.astro';
6
+ import NoteNavigationScript from './NoteNavigationScript.astro';
5
7
  import EditSourceLink from './EditSourceLink.astro';
8
+ import ImageStackEnhancement from './ImageStackEnhancement.astro';
6
9
  import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
7
10
  import SiteNavigation from './SiteNavigation.astro';
8
11
  import SiteBanners from './SiteBanners.astro';
@@ -10,8 +13,9 @@ import PageContentsNavigation from './PageContentsNavigation.astro';
10
13
  import PageSequenceNavigation from './PageSequenceNavigation.astro';
11
14
  import SiteSection from './SiteSection.astro';
12
15
  import SiteTreeNavigation from './SiteTreeNavigation.astro';
16
+ import TableOverflowScript from './TableOverflowScript.astro';
13
17
  import TreeNavigationScript from './TreeNavigationScript.astro';
14
- import { getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
18
+ import { flattenContentBlocks, getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
15
19
  import { getPageTheme } from '../lib/pageThemes';
16
20
  import { getSiteModel, type SitePage } from '../lib/sitePages';
17
21
  import {
@@ -27,22 +31,25 @@ import {
27
31
  resolvePageContentsPlacement,
28
32
  } from '../../scripts/lib/navigation-model.mjs';
29
33
  import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
30
- import { getEditSourceUrl } from '../../scripts/lib/edit-source-link.mjs';
34
+ import { resolveEditSourceTarget } from '../../scripts/lib/edit-source-link.mjs';
31
35
  import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
32
36
  import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
33
37
 
34
38
  type SiteEntry = CollectionEntry<'site'>;
35
39
 
36
40
  interface Props {
37
- entry: SiteEntry;
41
+ entry?: SiteEntry;
42
+ categoryPath?: string;
38
43
  }
39
44
 
40
- const { entry } = Astro.props;
45
+ const { entry, categoryPath } = Astro.props;
41
46
  const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
42
47
  const siteModel = await getSiteModel(await getCollection('site'));
43
48
  const { nodes: siteNodes, pages: sitePages } = siteModel;
44
- const currentPage = sitePages.find((page) => page.entry.id === entry.id);
45
- if (!currentPage) throw new Error(`Page entry "${entry.id}" is missing from the site page model.`);
49
+ const categoryDestination = categoryPath ? siteModel.categoryDestinations.byPathname.get(categoryPath) : null;
50
+ const currentPage = categoryDestination?.category
51
+ ?? sitePages.find((page) => page.entry.id === entry?.id);
52
+ if (!currentPage) throw new Error(`Page entry "${entry?.id ?? categoryPath}" is missing from the site page model.`);
46
53
  const [siteTheme] = await getCollection('theme');
47
54
  if (!siteTheme) {
48
55
  throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
@@ -91,11 +98,35 @@ const listedNavigationTree = getListedSiteNavigationTree(siteNavigation);
91
98
  const renderedNavigationEntries = flattenSiteNavigationTree(listedNavigationTree);
92
99
  const pageSequence = getSequentialPageNavigation(listedNavigationTree, currentPage.pagePath);
93
100
  const childPages = getDirectChildPages(listedNavigationTree, currentPage.pagePath);
94
- const siteHtml = entry.rendered?.html ?? '';
95
- const resolvedSections = await getSectionsContent(siteHtml, currentPage, childPages);
96
- const hasCodeBlocks = resolvedSections.some((section) => section.contentBlocks.some((block) => (
101
+ const siteHtml = entry?.rendered?.html ?? '';
102
+ const resolvedSections: ResolvedSection[] = categoryDestination ? [{
103
+ id: null,
104
+ title: currentPage.title,
105
+ titleHtml: currentPage.title.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;'),
106
+ headingLevel: 1,
107
+ contentBlocks: [{
108
+ type: 'page-list',
109
+ pages: categoryDestination.children.map((node) => ({
110
+ title: node.title,
111
+ pathname: node.pathname,
112
+ description: node.kind === 'page' ? node.entry.data.page?.description : undefined,
113
+ })),
114
+ }],
115
+ }] : await getSectionsContent(siteHtml, currentPage, childPages);
116
+ const allContentBlocks = resolvedSections.flatMap((section) => flattenContentBlocks(section.contentBlocks));
117
+ const hasTabs = allContentBlocks.some((block) => block.type === 'tabs');
118
+ const hasNotes = allContentBlocks.some((block) => (
119
+ block.type === 'html' && /class="[^"]*\bsection-note-ref\b|\bdata-footnote-ref(?:="")?/.test(block.html)
120
+ ));
121
+ const hasCodeBlocks = allContentBlocks.some((block) => (
97
122
  block.type === 'html' && /<pre\b[^>]*>[\s\S]*?<code\b/i.test(block.html)
98
- )));
123
+ ));
124
+ const hasTables = allContentBlocks.some((block) => (
125
+ block.type === 'html' && /\bdata-table-frame(?:="")?/i.test(block.html)
126
+ ));
127
+ const hasImageStack = allContentBlocks.some((block) => (
128
+ block.type === 'image-stack'
129
+ ));
99
130
  const hasPrioritizableImage = (section: ResolvedSection) => section.contentBlocks.some((block) => (
100
131
  block.type === 'image-stack'
101
132
  || block.type === 'image-carousel'
@@ -108,10 +139,13 @@ const renderedSections = resolvedSections.map((section, index) => ({
108
139
  prioritizeFirstImage: index === firstImageSectionIndex,
109
140
  }));
110
141
  const hasPageSequence = Boolean(pageSequence.previous || pageSequence.next);
111
- const editSourceUrl = projectConfig.editLink
112
- ? getEditSourceUrl({
113
- baseUrl: projectConfig.editLink.baseUrl,
114
- sourcePath: currentPage.contentLabel,
142
+ const editSourceTarget = projectConfig.editLink && currentPage.kind === 'page'
143
+ ? resolveEditSourceTarget({
144
+ ...projectConfig.editLink,
145
+ development: import.meta.env.DEV,
146
+ hostname: Astro.url.hostname,
147
+ sourceLabel: currentPage.contentLabel,
148
+ sourcePath: currentPage.contentPath,
115
149
  })
116
150
  : null;
117
151
  const navigationModel = resolveNavigationModel({
@@ -135,6 +169,7 @@ const pagePresentation = resolvePagePresentation(themeData, themeLabel, {
135
169
  const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
136
170
  const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: `${siteDirectory || 'site'}/${siteTheme.id}` });
137
171
  const hasTreeNavigation = navigationModel.mode === 'tree';
172
+ const showTreeNavigation = hasTreeNavigation && !currentPage.isHome;
138
173
  const currentPageNavigation = siteNavigation.find(({ node }) => (
139
174
  node.kind === 'page' && node.pathname === currentPage.pathname
140
175
  ));
@@ -154,27 +189,31 @@ const pageContentsPlacement = resolvePageContentsPlacement({
154
189
  headingCount: pageContentsHeadings.length,
155
190
  });
156
191
  const hasContentsNavigation = pageContentsPlacement.placement !== 'none';
157
- const integratePageContentsInTree = pageContentsPlacement.placement === 'page-tree';
192
+ const hasTrackedSectionNavigation = hasContentsNavigation
193
+ || (hasTreeNavigation && currentPage.isHome && pageContentsHeadings.length >= 2);
194
+ const integratePageContentsInTree = hasTreeNavigation;
158
195
  const showContentsRail = pageContentsPlacement.placement === 'contents-rail';
159
- const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
196
+ const hasNavigationRailFrame = !currentPage.isHome && (
197
+ navigationModel.requestedMode === 'tree'
160
198
  || (
161
199
  navigationModel.requestedMode === 'automatic'
162
200
  && (navigationModel.hasNestedPages || navigationModel.hasCategories)
163
- );
201
+ )
202
+ );
164
203
  const showBreadcrumbs = !currentPage.isHome && currentPage.parentPagePath !== null;
165
- const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
204
+ const reserveBreadcrumbSpace = showTreeNavigation || showBreadcrumbs;
166
205
 
167
206
  ---
168
207
 
169
208
  <BaseLayout
170
209
  title={currentPage.title}
171
- description={entry.data.page?.description}
210
+ description={entry?.data.page?.description}
172
211
  presentation={pagePresentation}
173
212
  visualTheme={visualTheme}
174
213
  frameColors={frameColors}
175
214
  pathname={currentPage.pathname}
176
215
  footer={sitewideData.footer}
177
- contentsNavigation={hasContentsNavigation}
216
+ contentsNavigation={hasTrackedSectionNavigation}
178
217
  searchable
179
218
  >
180
219
  <SiteNavigation
@@ -190,12 +229,12 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
190
229
  {siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
191
230
 
192
231
  <div class:list={['site-page-layout', {
193
- 'site-page-layout-tree': hasTreeNavigation,
232
+ 'site-page-layout-tree': showTreeNavigation,
194
233
  'site-page-layout-contents': showContentsRail,
195
234
  }]}
196
235
  data-page-contents-placement={hasContentsNavigation ? pageContentsPlacement.placement : undefined}
197
236
  >
198
- {hasTreeNavigation && (
237
+ {showTreeNavigation && (
199
238
  <SiteTreeNavigation
200
239
  currentPage={currentPage}
201
240
  navigationEntries={siteNavigation}
@@ -205,7 +244,7 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
205
244
  <main
206
245
  class:list={['site-content', {
207
246
  'site-content-has-breadcrumbs': reserveBreadcrumbSpace,
208
- 'site-content-has-edit-source': Boolean(editSourceUrl),
247
+ 'site-content-has-edit-source': Boolean(editSourceTarget),
209
248
  'site-content-has-page-sequence': hasPageSequence,
210
249
  }]}
211
250
  id="main-content"
@@ -215,13 +254,6 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
215
254
  {showBreadcrumbs
216
255
  ? <SiteBreadcrumbs currentPage={currentPage} nodes={siteNodes} />
217
256
  : reserveBreadcrumbSpace && <div class="site-breadcrumbs" aria-hidden="true"></div>}
218
- {showContentsRail && (
219
- <PageContentsNavigation
220
- headings={pageContentsHeadings}
221
- pageTitle={currentPage.title}
222
- variant="inline"
223
- />
224
- )}
225
257
  {renderedSections.map(({ section, sectionIndex, prioritizeFirstImage }) => (
226
258
  <SiteSection
227
259
  section={section}
@@ -231,8 +263,13 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
231
263
  prioritizeFirstImage={prioritizeFirstImage}
232
264
  />
233
265
  ))}
234
- {editSourceUrl && (
235
- <EditSourceLink href={editSourceUrl} label={projectConfig.locale.labels.editSource} />
266
+ {editSourceTarget && (
267
+ <EditSourceLink
268
+ href={editSourceTarget.href}
269
+ label={editSourceTarget.kind === 'local'
270
+ ? projectConfig.locale.labels.openInVsCode
271
+ : projectConfig.locale.labels.editSource}
272
+ />
236
273
  )}
237
274
  <PageSequenceNavigation previous={pageSequence.previous} next={pageSequence.next} />
238
275
  </main>
@@ -240,10 +277,13 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
240
277
  <PageContentsNavigation
241
278
  headings={pageContentsHeadings}
242
279
  pageTitle={currentPage.title}
243
- variant="rail"
244
280
  />
245
281
  )}
246
282
  </div>
247
283
  {hasTreeNavigation && <TreeNavigationScript />}
248
284
  {hasCodeBlocks && <CodeBlockCopyScript />}
285
+ {hasTabs && <ContentTabsScript />}
286
+ {hasNotes && <NoteNavigationScript />}
287
+ {hasTables && <TableOverflowScript />}
288
+ {hasImageStack && <ImageStackEnhancement />}
249
289
  </BaseLayout>
@@ -6,11 +6,8 @@ import {
6
6
  getHeadingTypographySizeValue,
7
7
  getTypographySizeValue,
8
8
  } from '../../scripts/lib/typography.mjs';
9
- import CardList from './CardList.astro';
10
- import ImageCarousel from './ImageCarousel.astro';
11
- import ImageStack from './ImageStack.astro';
12
- import PageList from './PageList.astro';
13
- import type { ResolvedSection } from '../lib/sectionContent';
9
+ import ContentBlocks from './ContentBlocks.astro';
10
+ import { flattenContentBlocks, type ResolvedSection } from '../lib/sectionContent';
14
11
 
15
12
  type ResponsiveValue<T> = { desktop: T; mobile: T };
16
13
  type ResponsiveOverride<T> = { desktop?: T; mobile?: T };
@@ -110,42 +107,11 @@ const bodyWidth = {
110
107
  desktop: bodyWidthValue,
111
108
  mobile: bodyWidthValue,
112
109
  };
113
- const isStructuredBlock = (block: ResolvedSection['contentBlocks'][number]) => (
114
- block.type === 'image-stack'
115
- || block.type === 'image-carousel'
116
- || block.type === 'card-list'
117
- || block.type === 'page-list'
118
- );
119
- const hasMedia = section.contentBlocks.some((block) => (
110
+ const hasMedia = flattenContentBlocks(section.contentBlocks).some((block) => (
120
111
  block.type === 'image-stack'
121
112
  || block.type === 'image-carousel'
122
113
  || (block.type === 'card-list' && block.cards.some((card) => card.image))
123
114
  ));
124
- const contentGroups: Array<
125
- | { type: 'reading'; blocks: ResolvedSection['contentBlocks'] }
126
- | { type: 'structured'; block: Extract<ResolvedSection['contentBlocks'][number], { type: 'image-stack' | 'image-carousel' | 'card-list' | 'page-list' }> }
127
- > = [];
128
- let readingBlocks: ResolvedSection['contentBlocks'] = [];
129
- const flushReadingBlocks = () => {
130
- if (readingBlocks.length === 0) return;
131
- contentGroups.push({ type: 'reading', blocks: readingBlocks });
132
- readingBlocks = [];
133
- };
134
-
135
- for (const block of section.contentBlocks) {
136
- if (isStructuredBlock(block)) {
137
- flushReadingBlocks();
138
- contentGroups.push({ type: 'structured', block });
139
- } else {
140
- readingBlocks = [...readingBlocks, block];
141
- }
142
- }
143
-
144
- flushReadingBlocks();
145
- const firstMediaGroupIndex = contentGroups.findIndex((group) => (
146
- group.type === 'structured'
147
- && (group.block.type === 'image-stack' || group.block.type === 'image-carousel')
148
- ));
149
115
  const markdownHeadingStyle = (level: 'h3' | 'h4') => {
150
116
  const headingConfig = markdownHeadings[level];
151
117
  const fontSize = getHeadingTypographySizeValue(level, headingConfig.size);
@@ -209,55 +175,13 @@ const sectionStyle = [
209
175
  style={sectionStyle}
210
176
  >
211
177
  <div class="section-card">
178
+ <slot name="before-heading" />
212
179
  <header class="section-header">
213
180
  <HeadingTag id={headingId} set:html={section.titleHtml} />
214
181
  </header>
215
182
 
216
183
  <div class:list={['section-body', { 'section-body-has-media': hasMedia, 'section-body-text-only': !hasMedia }]}>
217
- {contentGroups.map((group, groupIndex) => {
218
- if (group.type === 'reading') {
219
- return group.blocks.map((block) => block.type === 'html'
220
- && block.html
221
- && <div class="section-markdown" set:html={block.html} />);
222
- }
223
-
224
- const block = group.block;
225
- if (block.type === 'image-carousel') {
226
- return (
227
- <ImageCarousel
228
- title={section.title}
229
- images={block.images}
230
- maxAvailableWidthPercent={visualTheme.images.maxAvailableWidthPercent}
231
- maxAvailableHeightPercent={visualTheme.images.maxAvailableHeightPercent}
232
- priorityFirstImage={prioritizeFirstImage && groupIndex === firstMediaGroupIndex}
233
- />
234
- );
235
- }
236
-
237
- if (block.type === 'card-list') {
238
- return (
239
- <CardList
240
- cards={block.cards}
241
- layout={block.layout}
242
- flow={block.flow}
243
- size={block.size}
244
- width={block.width ?? pagePresentation.blocks.cardList.width}
245
- />
246
- );
247
- }
248
- if (block.type === 'page-list') {
249
- return <PageList pages={block.pages} />;
250
- }
251
-
252
- return (
253
- <ImageStack
254
- title={section.title}
255
- images={block.images}
256
- maxAvailableWidthPercent={visualTheme.images.maxAvailableWidthPercent}
257
- priorityFirstImage={prioritizeFirstImage && groupIndex === firstMediaGroupIndex}
258
- />
259
- );
260
- })}
184
+ <ContentBlocks blocks={section.contentBlocks} title={section.title} images={visualTheme.images} cardWidth={pagePresentation.blocks.cardList.width} prioritizeFirstImage={prioritizeFirstImage} />
261
185
  </div>
262
186
  </div>
263
187
  </section>
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { withBasePath } from '../lib/basePath';
3
4
  import { getListedSiteNavigationTree, type SiteNavigationEntry } from '../lib/siteNavigation';
4
- import type { SitePage } from '../lib/sitePages';
5
+ import type { SiteNode } from '../lib/sitePages';
5
6
  import NavigationPageTree from './NavigationPageTree.astro';
6
7
  import NavigationTreeControls from './NavigationTreeControls.astro';
7
8
 
8
9
  interface Props {
9
- currentPage: SitePage;
10
+ currentPage: SiteNode;
10
11
  navigationEntries: SiteNavigationEntry[];
11
12
  integratePageSections?: boolean;
12
13
  }
@@ -18,6 +19,7 @@ const activeRoot = roots.find((node) => (
18
19
  || currentPage.pageDirectory.startsWith(`${node.node.pageDirectory}/pages/`)
19
20
  ));
20
21
  const navigationRoot = activeRoot?.node.pagePath ? `/${activeRoot.node.pagePath}/` : '/';
22
+ const navigationStateRoot = withBasePath(projectConfig.site.basePath, '/');
21
23
  ---
22
24
 
23
25
  {activeRoot && (
@@ -25,6 +27,7 @@ const navigationRoot = activeRoot?.node.pagePath ? `/${activeRoot.node.pagePath}
25
27
  id="tree-local-navigation"
26
28
  class="tree-local-navigation"
27
29
  data-navigation-root={navigationRoot}
30
+ data-navigation-state-root={navigationStateRoot}
28
31
  >
29
32
  <NavigationTreeControls
30
33
  idPrefix="desktop-tree-navigation"