@janga/norna 0.7.25 → 0.7.27

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 (137) hide show
  1. package/README.md +18 -10
  2. package/astro.config.mjs +26 -12
  3. package/bin/norna-cli.mjs +14 -2
  4. package/package.json +14 -5
  5. package/schemas/card-list.schema.json +121 -0
  6. package/schemas/category.schema.json +4 -4
  7. package/schemas/config.schema.json +306 -32
  8. package/schemas/content-frontmatter.schema.json +8 -8
  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/add-site-node.mjs +1 -1
  16. package/scripts/dev-local.mjs +24 -2
  17. package/scripts/generate-schemas.mjs +15 -3
  18. package/scripts/lib/category-destinations.mjs +48 -0
  19. package/scripts/lib/code-fence-metadata.mjs +14 -37
  20. package/scripts/lib/content-tabs.mjs +155 -0
  21. package/scripts/lib/details-headings.mjs +58 -0
  22. package/scripts/lib/documentation-links.mjs +33 -2
  23. package/scripts/lib/documentation-routes.json +222 -0
  24. package/scripts/lib/docusaurus-migration-audit.mjs +603 -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/norna-markdown-blocks.mjs +32 -447
  78. package/scripts/lib/norna-markdown-render-plugin.mjs +4 -0
  79. package/scripts/lib/page-aliases.mjs +12 -1
  80. package/scripts/lib/page-markdown.mjs +63 -16
  81. package/scripts/lib/page-model.mjs +22 -0
  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 +2 -173
  87. package/scripts/lib/schema-definitions.mjs +6 -13
  88. package/scripts/lib/schema-editor-metadata.mjs +23 -44
  89. package/scripts/lib/schema-value-definitions.mjs +4 -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/sitemap.mjs +5 -3
  95. package/scripts/lib/structured-blocks.mjs +108 -0
  96. package/scripts/lib/table-render-plugin.mjs +46 -3
  97. package/scripts/lib/table-row-headers.mjs +218 -0
  98. package/scripts/lib/theme-presets.mjs +3 -10
  99. package/scripts/lib/yaml-config.mjs +2 -6
  100. package/scripts/migrate-check.mjs +19 -0
  101. package/scripts/sync-content-sections.mjs +39 -10
  102. package/scripts/sync-site-public.mjs +7 -1
  103. package/src/components/CodeBlockCopyScript.astro +47 -5
  104. package/src/components/ContentBlocks.astro +39 -0
  105. package/src/components/ContentTabsScript.astro +60 -0
  106. package/src/components/DisplaySettings.astro +17 -19
  107. package/src/components/NavigationPageTree.astro +1 -1
  108. package/src/components/NoteNavigationScript.astro +4 -0
  109. package/src/components/PageAliasRedirect.astro +49 -7
  110. package/src/components/SearchNavigationScript.astro +4 -0
  111. package/src/components/SearchPage.astro +43 -2
  112. package/src/components/SectionNavigationScript.astro +43 -11
  113. package/src/components/SiteBreadcrumbs.astro +1 -1
  114. package/src/components/SiteNavigation.astro +29 -20
  115. package/src/components/SitePage.astro +40 -15
  116. package/src/components/SiteSection.astro +5 -81
  117. package/src/components/SiteTreeNavigation.astro +2 -2
  118. package/src/components/TableOverflowScript.astro +386 -58
  119. package/src/components/TopPageMenu.astro +47 -0
  120. package/src/components/TreeNavigationScript.astro +10 -3
  121. package/src/content.config.ts +5 -8
  122. package/src/layouts/BaseLayout.astro +8 -12
  123. package/src/lib/navigationFollowing.ts +142 -0
  124. package/src/lib/noteNavigation.ts +50 -0
  125. package/src/lib/searchNavigation.ts +107 -0
  126. package/src/lib/sectionContent.ts +20 -90
  127. package/src/lib/siteNavigation.ts +0 -5
  128. package/src/lib/sitePages.ts +17 -20
  129. package/src/lib/table-sort-tooltip.ts +109 -0
  130. package/src/pages/[...slug].astro +16 -6
  131. package/src/styles/content.css +397 -134
  132. package/src/styles/foundations.css +9 -0
  133. package/src/styles/media.css +5 -0
  134. package/src/styles/navigation.css +63 -74
  135. package/src/styles/page-layout.css +7 -0
  136. package/src/styles/responsive.css +2 -7
  137. package/starters/basic/README.md +1 -1
@@ -4,16 +4,17 @@ 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}
@@ -211,18 +219,18 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
211
219
  <h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
212
220
  <NavigationTreeControls
213
221
  idPrefix="mobile-tree-navigation"
214
- nodes={siteNavigationTree}
222
+ nodes={mobileNavigationTree}
215
223
  />
216
224
  <NavigationPageTree
217
- nodes={siteNavigationTree}
225
+ nodes={mobileNavigationTree}
218
226
  currentPage={currentPage}
219
227
  variant="mobile"
220
- integratePageSections={navigationMode === 'tree'}
228
+ integratePageSections={navigationMode === 'tree' || navigationMode === 'top'}
221
229
  />
222
230
  </nav>
223
231
  )}
224
232
 
225
- {navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
233
+ {(navigationMode === 'sections' || !showSiteNavigation) && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
226
234
  <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
227
235
  {!showSinglePageTopLink && (
228
236
  <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
@@ -285,3 +293,4 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
285
293
  </header>
286
294
 
287
295
  {needsNavigationScript && <SectionNavigationScript />}
296
+ {showSearch && <SearchNavigationScript />}
@@ -2,6 +2,8 @@
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';
6
8
  import ImageStackEnhancement from './ImageStackEnhancement.astro';
7
9
  import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
@@ -13,7 +15,7 @@ import SiteSection from './SiteSection.astro';
13
15
  import SiteTreeNavigation from './SiteTreeNavigation.astro';
14
16
  import TableOverflowScript from './TableOverflowScript.astro';
15
17
  import TreeNavigationScript from './TreeNavigationScript.astro';
16
- import { getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
18
+ import { flattenContentBlocks, getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
17
19
  import { getPageTheme } from '../lib/pageThemes';
18
20
  import { getSiteModel, type SitePage } from '../lib/sitePages';
19
21
  import {
@@ -36,15 +38,18 @@ import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
36
38
  type SiteEntry = CollectionEntry<'site'>;
37
39
 
38
40
  interface Props {
39
- entry: SiteEntry;
41
+ entry?: SiteEntry;
42
+ categoryPath?: string;
40
43
  }
41
44
 
42
- const { entry } = Astro.props;
45
+ const { entry, categoryPath } = Astro.props;
43
46
  const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
44
47
  const siteModel = await getSiteModel(await getCollection('site'));
45
48
  const { nodes: siteNodes, pages: sitePages } = siteModel;
46
- const currentPage = sitePages.find((page) => page.entry.id === entry.id);
47
- 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.`);
48
53
  const [siteTheme] = await getCollection('theme');
49
54
  if (!siteTheme) {
50
55
  throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
@@ -93,17 +98,35 @@ const listedNavigationTree = getListedSiteNavigationTree(siteNavigation);
93
98
  const renderedNavigationEntries = flattenSiteNavigationTree(listedNavigationTree);
94
99
  const pageSequence = getSequentialPageNavigation(listedNavigationTree, currentPage.pagePath);
95
100
  const childPages = getDirectChildPages(listedNavigationTree, currentPage.pagePath);
96
- const siteHtml = entry.rendered?.html ?? '';
97
- const resolvedSections = await getSectionsContent(siteHtml, currentPage, childPages);
98
- 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) => (
99
122
  block.type === 'html' && /<pre\b[^>]*>[\s\S]*?<code\b/i.test(block.html)
100
- )));
101
- const hasTables = resolvedSections.some((section) => section.contentBlocks.some((block) => (
123
+ ));
124
+ const hasTables = allContentBlocks.some((block) => (
102
125
  block.type === 'html' && /\bdata-table-frame(?:="")?/i.test(block.html)
103
- )));
104
- const hasImageStack = resolvedSections.some((section) => section.contentBlocks.some((block) => (
126
+ ));
127
+ const hasImageStack = allContentBlocks.some((block) => (
105
128
  block.type === 'image-stack'
106
- )));
129
+ ));
107
130
  const hasPrioritizableImage = (section: ResolvedSection) => section.contentBlocks.some((block) => (
108
131
  block.type === 'image-stack'
109
132
  || block.type === 'image-carousel'
@@ -116,7 +139,7 @@ const renderedSections = resolvedSections.map((section, index) => ({
116
139
  prioritizeFirstImage: index === firstImageSectionIndex,
117
140
  }));
118
141
  const hasPageSequence = Boolean(pageSequence.previous || pageSequence.next);
119
- const editSourceTarget = projectConfig.editLink
142
+ const editSourceTarget = projectConfig.editLink && currentPage.kind === 'page'
120
143
  ? resolveEditSourceTarget({
121
144
  ...projectConfig.editLink,
122
145
  development: import.meta.env.DEV,
@@ -184,7 +207,7 @@ const reserveBreadcrumbSpace = showTreeNavigation || showBreadcrumbs;
184
207
 
185
208
  <BaseLayout
186
209
  title={currentPage.title}
187
- description={entry.data.page?.description}
210
+ description={entry?.data.page?.description}
188
211
  presentation={pagePresentation}
189
212
  visualTheme={visualTheme}
190
213
  frameColors={frameColors}
@@ -259,6 +282,8 @@ const reserveBreadcrumbSpace = showTreeNavigation || showBreadcrumbs;
259
282
  </div>
260
283
  {hasTreeNavigation && <TreeNavigationScript />}
261
284
  {hasCodeBlocks && <CodeBlockCopyScript />}
285
+ {hasTabs && <ContentTabsScript />}
286
+ {hasNotes && <NoteNavigationScript />}
262
287
  {hasTables && <TableOverflowScript />}
263
288
  {hasImageStack && <ImageStackEnhancement />}
264
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>
@@ -2,12 +2,12 @@
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
3
  import { withBasePath } from '../lib/basePath';
4
4
  import { getListedSiteNavigationTree, type SiteNavigationEntry } from '../lib/siteNavigation';
5
- import type { SitePage } from '../lib/sitePages';
5
+ import type { SiteNode } from '../lib/sitePages';
6
6
  import NavigationPageTree from './NavigationPageTree.astro';
7
7
  import NavigationTreeControls from './NavigationTreeControls.astro';
8
8
 
9
9
  interface Props {
10
- currentPage: SitePage;
10
+ currentPage: SiteNode;
11
11
  navigationEntries: SiteNavigationEntry[];
12
12
  integratePageSections?: boolean;
13
13
  }