@janga/norna 0.7.9 → 0.7.10

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 (198) hide show
  1. package/README.md +1 -1
  2. package/docs/README.md +14 -11
  3. package/docs/configuration.md +9 -54
  4. package/docs/content.md +6 -7
  5. package/docs/design/norna-diagram-design.md +2 -0
  6. package/docs/getting-started.md +18 -16
  7. package/docs/routes.md +7 -6
  8. package/docs/site-structure.md +4 -2
  9. package/docs/sitewide-content.md +74 -0
  10. package/docs/theme.md +61 -128
  11. package/docs/typography.md +45 -33
  12. package/examples/001-dog-shelter-single-page/README.md +9 -0
  13. package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
  14. package/examples/001-dog-shelter-single-page/site/content.md +38 -0
  15. package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
  16. package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
  17. package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
  18. package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
  19. package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
  20. package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
  21. package/examples/002-dog-shelter-multi-page/README.md +9 -0
  22. package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
  23. package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
  24. package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
  25. package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
  26. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
  27. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
  28. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
  29. package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
  30. package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
  31. package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
  32. package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
  33. package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
  34. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  35. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  36. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  37. package/examples/dog-gallery/.astro/data-store.json +1 -0
  38. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  39. package/examples/dog-gallery/.astro/dev.json +13 -0
  40. package/examples/dog-gallery/.astro/dev.log +95 -0
  41. package/examples/dog-gallery/.astro/settings.json +5 -0
  42. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  43. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  44. package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
  45. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  46. package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
  47. package/examples/dog-gallery/dist/favicon.svg +7 -0
  48. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  75. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  76. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  77. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  78. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  79. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  80. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  81. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  82. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  83. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  84. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  85. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  86. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  87. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  88. package/examples/dog-gallery/dist/index.html +45 -0
  89. package/examples/dog-gallery/dist/robots.txt +2 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
  91. package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
  92. package/examples/dog-gallery/site/config.mjs +0 -20
  93. package/examples/dog-gallery/site/content.md +4 -5
  94. package/examples/dog-gallery/site/public/logo.svg +14 -0
  95. package/examples/dog-gallery/site/sitewide-content.md +16 -0
  96. package/examples/dog-gallery/site/theme.md +13 -24
  97. package/examples/presentation/README.md +15 -0
  98. package/examples/presentation/site/.norna/generated-images.json +66 -0
  99. package/examples/presentation/site/config.mjs +29 -0
  100. package/examples/presentation/site/content.md +67 -0
  101. package/examples/presentation/site/public/robots.txt +2 -0
  102. package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
  103. package/examples/presentation/site/routes/010-presets/theme.md +4 -0
  104. package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
  105. package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
  106. package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
  107. package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
  108. package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
  109. package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
  110. package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
  111. package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
  112. package/examples/presentation/site/routes/020-media/route-content.md +77 -0
  113. package/examples/presentation/site/routes/020-media/theme.md +4 -0
  114. package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
  115. package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
  116. package/examples/presentation/site/sitewide-content.md +6 -0
  117. package/examples/presentation/site/theme.md +43 -0
  118. package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
  119. package/examples/typography-presets/.astro/collections/site.schema.json +0 -717
  120. package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
  121. package/examples/typography-presets/.astro/collections/theme.schema.json +161 -59
  122. package/examples/typography-presets/.astro/content.d.ts +8 -0
  123. package/examples/typography-presets/.astro/data-store.json +1 -1
  124. package/examples/typography-presets/.astro/dev-local.json +2 -2
  125. package/examples/typography-presets/.astro/dev.json +2 -2
  126. package/examples/typography-presets/.astro/dev.log +37 -16
  127. package/examples/typography-presets/site/.norna/generated-images.json +76 -76
  128. package/examples/typography-presets/site/config.mjs +0 -13
  129. package/examples/typography-presets/site/content.md +8 -126
  130. package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
  131. package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
  132. package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
  133. package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
  134. package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
  135. package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
  136. package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
  137. package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
  138. package/examples/typography-presets/site/sitewide-content.md +14 -0
  139. package/examples/typography-presets/site/theme.md +9 -9
  140. package/fixtures/basic/site/config.mjs +0 -13
  141. package/fixtures/basic/site/sitewide-content.md +12 -0
  142. package/fixtures/basic/site/theme.md +2 -2
  143. package/fixtures/content-model-v2/site/content.md +2 -5
  144. package/fixtures/content-model-v2/site/theme.md +1 -3
  145. package/package.json +3 -2
  146. package/scripts/check-config.mjs +22 -1
  147. package/scripts/lib/frontmatter-yaml.mjs +28 -0
  148. package/scripts/lib/logo-assets.mjs +35 -0
  149. package/scripts/lib/norna-markdown-blocks.mjs +166 -0
  150. package/scripts/lib/presentation.mjs +104 -46
  151. package/scripts/lib/project-config.mjs +67 -105
  152. package/scripts/lib/site-content.mjs +26 -118
  153. package/scripts/lib/site-paths.mjs +2 -0
  154. package/scripts/lib/sitewide-config.mjs +34 -0
  155. package/scripts/lib/theme-config.mjs +72 -8
  156. package/scripts/lib/typography.mjs +20 -26
  157. package/scripts/show-typography.mjs +40 -147
  158. package/scripts/sync-content-sections.mjs +19 -17
  159. package/scripts/test-content-check.mjs +59 -27
  160. package/scripts/test-content-model-v2.mjs +154 -0
  161. package/scripts/test-engine-commands.mjs +3 -1
  162. package/scripts/test-package-check.mjs +66 -24
  163. package/scripts/test-temporary-visibility.mjs +19 -2
  164. package/src/components/CardList.astro +5 -2
  165. package/src/components/GalleryGrid.astro +37 -16
  166. package/src/components/SiteBanners.astro +100 -0
  167. package/src/components/SiteNavigation.astro +48 -31
  168. package/src/components/SitePage.astro +55 -13
  169. package/src/components/SiteSection.astro +193 -35
  170. package/src/content.config.ts +78 -48
  171. package/src/layouts/BaseLayout.astro +64 -14
  172. package/src/lib/routeThemes.ts +61 -0
  173. package/src/lib/sectionContent.ts +235 -51
  174. package/src/lib/sitePublicAssets.ts +1 -0
  175. package/src/styles/global.css +282 -93
  176. package/starters/basic/.github/workflows/deploy.yml +1 -1
  177. package/starters/basic/README.md +7 -6
  178. package/starters/basic/site/config.mjs +0 -13
  179. package/starters/basic/site/content.md +1 -1
  180. package/starters/basic/site/sitewide-content.md +14 -0
  181. package/starters/basic/site/theme.md +13 -24
  182. package/starters/project/.github/workflows/deploy.yml +1 -1
  183. package/starters/project/README.md +10 -8
  184. package/starters/project/site/config.mjs +0 -13
  185. package/starters/project/site/sitewide-content.md +14 -0
  186. package/starters/project/site/theme.md +3 -11
  187. /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/puppy-portrait.jpg +0 -0
  188. /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/shepherd-portrait.jpg +0 -0
  189. /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/white-puppy.jpg +0 -0
  190. /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/black-schnauzer.jpg +0 -0
  191. /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/retriever-portrait.jpg +0 -0
  192. /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/schnauzer-portrait.jpg +0 -0
  193. /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/boxer-portrait.jpg +0 -0
  194. /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grass-puppy.jpg +0 -0
  195. /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grey-street-dog.jpg +0 -0
  196. /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/black-pedigree.jpg +0 -0
  197. /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-grass.jpg +0 -0
  198. /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-wide.jpg +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
3
  import { withBasePath } from '../lib/basePath';
4
- import type { ResolvedSection } from '../lib/sectionContent';
4
+ import type { SectionNavigation } from '../lib/sectionContent';
5
5
  import type { SitePage } from '../lib/sitePages';
6
6
 
7
7
  type FrameColors = {
@@ -11,13 +11,20 @@ type FrameColors = {
11
11
 
12
12
  interface Props {
13
13
  brandLabel: string;
14
+ logo?: {
15
+ src: string;
16
+ alt: string;
17
+ height?: string;
18
+ };
14
19
  currentPage: SitePage;
15
- pages: SitePage[];
16
- sections: ResolvedSection[];
20
+ pageNavigation: Array<{
21
+ page: SitePage;
22
+ sections: SectionNavigation[];
23
+ }>;
17
24
  frameColors: FrameColors;
18
25
  }
19
26
 
20
- const { brandLabel, currentPage, pages, sections, frameColors } = Astro.props;
27
+ const { brandLabel, logo, currentPage, pageNavigation, frameColors } = Astro.props;
21
28
  const smoothScrollConfig = projectConfig.navigation.smoothScroll;
22
29
  const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
23
30
  ? 'var(--color-page)'
@@ -25,12 +32,15 @@ const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
25
32
  const navigationStyle = [
26
33
  `--site-top-background-color: ${siteTopBackgroundColor}`,
27
34
  `--site-top-text-color: ${frameColors.textColor}`,
35
+ ...(logo?.height ? [`--site-brand-logo-height: ${logo.height}`] : []),
28
36
  ].join('; ');
29
- const showSiteNavigation = pages.length > 1;
30
- const showPageNavigation = sections.length > 1;
31
- const homePage = pages.find((page) => page.isHome) ?? currentPage;
37
+ const showSiteNavigation = pageNavigation.length > 1;
38
+ const homePage = pageNavigation.find(({ page }) => page.isHome)?.page ?? currentPage;
39
+ const currentPageNavigation = pageNavigation.find(({ page }) => page.pathname === currentPage.pathname);
32
40
  const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
33
41
  const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
42
+ const sectionHref = (page: SitePage, section: SectionNavigation) => `${pageHref(page)}#${section.id}`;
43
+ const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
34
44
  ---
35
45
 
36
46
  <a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
@@ -42,15 +52,30 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
42
52
  >
43
53
  {showSiteNavigation && (
44
54
  <div class="site-nav-row">
45
- <a class="site-brand" href={pageHref(homePage)}>{brandLabel}</a>
55
+ <a class="site-brand" href={pageHref(homePage)}>
56
+ {logo ? <img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} /> : brandLabel}
57
+ </a>
46
58
 
47
59
  <nav class="site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
48
60
  <ul>
49
- {pages.map((page) => (
50
- <li>
61
+ {pageNavigation.map(({ page, sections: pageSections }) => (
62
+ <li class:list={['site-nav-item', { 'site-nav-item-has-sections': hasSectionMenu(pageSections) }]}>
51
63
  <a href={pageHref(page)} aria-current={isCurrentPage(page) ? 'page' : undefined}>
52
64
  {page.navigation.label}
53
65
  </a>
66
+ {hasSectionMenu(pageSections) && (
67
+ <div class="site-nav-submenu">
68
+ <nav aria-label={`${page.navigation.label} ${projectConfig.locale.labels.pageNavigation}`}>
69
+ <ul>
70
+ {pageSections.map((section) => (
71
+ <li>
72
+ <a href={sectionHref(page, section)}>{section.title}</a>
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ </nav>
77
+ </div>
78
+ )}
54
79
  </li>
55
80
  ))}
56
81
  </ul>
@@ -64,7 +89,7 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
64
89
  <nav class="mobile-site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
65
90
  <h2>{projectConfig.locale.labels.siteNavigation}</h2>
66
91
  <ul>
67
- {pages.map((page) => (
92
+ {pageNavigation.map(({ page }) => (
68
93
  <li>
69
94
  <a href={pageHref(page)} aria-current={isCurrentPage(page) ? 'page' : undefined}>
70
95
  {page.navigation.label}
@@ -74,13 +99,13 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
74
99
  </ul>
75
100
  </nav>
76
101
 
77
- {showPageNavigation && (
102
+ {currentPageNavigation && hasSectionMenu(currentPageNavigation.sections) && (
78
103
  <nav class="mobile-page-nav" aria-label={projectConfig.locale.labels.pageNavigation}>
79
104
  <h2>{projectConfig.locale.labels.pageNavigation}</h2>
80
105
  <ul>
81
- {sections.map((section, index) => (
106
+ {currentPageNavigation.sections.map((section, index) => (
82
107
  <li>
83
- <a href={`#${section.id}`} aria-current={index === 0 ? 'true' : undefined}>
108
+ <a href={sectionHref(currentPage, section)} aria-current={index === 0 ? 'true' : undefined}>
84
109
  {section.title}
85
110
  </a>
86
111
  </li>
@@ -93,25 +118,12 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
93
118
  </div>
94
119
  )}
95
120
 
96
- {showPageNavigation && (
97
- <nav class="page-nav" aria-label={projectConfig.locale.labels.pageNavigation}>
98
- <ul>
99
- {sections.map((section, index) => (
100
- <li>
101
- <a href={`#${section.id}`} aria-current={index === 0 ? 'true' : undefined}>
102
- {section.title}
103
- </a>
104
- </li>
105
- ))}
106
- </ul>
107
- </nav>
108
- )}
109
121
  </header>
110
122
 
111
123
  <script>
112
124
  const siteTop = document.querySelector('.site-top');
113
125
  const sections = Array.from(document.querySelectorAll('.site-section'));
114
- const links = Array.from(document.querySelectorAll('.page-nav a, .mobile-page-nav a'));
126
+ const links = Array.from(document.querySelectorAll('.site-nav-submenu a, .mobile-page-nav a'));
115
127
  const mobileMenu = document.querySelector('.mobile-nav-menu');
116
128
  const smoothScrollConfig = JSON.parse(siteTop?.getAttribute('data-smooth-scroll') ?? '{}');
117
129
  const anchorPadding = 0;
@@ -159,7 +171,9 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
159
171
  activeSectionId = section.id;
160
172
 
161
173
  links.forEach((link) => {
162
- const isActive = link.getAttribute('href') === `#${section.id}`;
174
+ const linkUrl = new URL(link.href, window.location.href);
175
+ const isCurrentPageLink = linkUrl.pathname === window.location.pathname;
176
+ const isActive = isCurrentPageLink && linkUrl.hash === `#${section.id}`;
163
177
  if (isActive) {
164
178
  link.setAttribute('aria-current', 'true');
165
179
  } else {
@@ -372,7 +386,10 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
372
386
 
373
387
  links.forEach((link) => {
374
388
  link.addEventListener('click', (event) => {
375
- const section = getSectionFromHash(link.hash);
389
+ const linkUrl = new URL(link.href, window.location.href);
390
+ if (linkUrl.pathname !== window.location.pathname) return;
391
+
392
+ const section = getSectionFromHash(linkUrl.hash);
376
393
  if (!(section instanceof HTMLElement)) return;
377
394
 
378
395
  const top = getSectionScrollTop(section);
@@ -380,7 +397,7 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
380
397
 
381
398
  event.preventDefault();
382
399
 
383
- const targetHash = `#${section.id}`;
400
+ const targetHash = linkUrl.hash;
384
401
  const duration = getScrollDuration(top - window.scrollY);
385
402
 
386
403
  setHash(targetHash);
@@ -3,12 +3,16 @@ import { getCollection, type CollectionEntry } from 'astro:content';
3
3
  import path from 'node:path';
4
4
  import BaseLayout from '../layouts/BaseLayout.astro';
5
5
  import SiteNavigation from './SiteNavigation.astro';
6
+ import SiteBanners from './SiteBanners.astro';
6
7
  import SiteSection from './SiteSection.astro';
7
- import { getSectionsContent } from '../lib/sectionContent';
8
+ import { getSectionNavigation, getSectionsContent } from '../lib/sectionContent';
9
+ import { getRouteTheme } from '../lib/routeThemes';
8
10
  import { getNavigationPages, getSitePage, getSitePages } from '../lib/sitePages';
9
11
  import { getToday, isVisibleToday } from '../lib/visibility';
12
+ import { getLogoAsset } from '../lib/sitePublicAssets';
10
13
  import { readSiteFile } from '../../scripts/lib/site-content.mjs';
11
14
  import { siteContentPath, siteRoutesDir } from '../../scripts/lib/site-paths.mjs';
15
+ import { resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
12
16
  import { resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
13
17
 
14
18
  type SiteEntry = CollectionEntry<'site'>;
@@ -28,23 +32,55 @@ const pageContentPath = currentPage.routeDirectory
28
32
  ? path.join(siteRoutesDir, currentPage.routeDirectory, 'route-content.md')
29
33
  : siteContentPath;
30
34
  const rawMarkdown = (await readSiteFile(pageContentPath, siteContentLabel)).body;
31
- const [theme] = await getCollection('theme');
35
+ const [siteTheme] = await getCollection('theme');
36
+ if (!siteTheme) {
37
+ throw new Error('site/theme.md is required. Add a root theme before building the site.');
38
+ }
39
+ const [sitewide] = await getCollection('sitewide');
40
+ const sitewideData = sitewide?.data ?? {};
32
41
  const navigationPages = getNavigationPages(sitePages);
33
42
  const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
34
- const themeData = theme?.data ?? {};
35
- const brandLabel = themeData.navigation?.brand ?? homePage.title;
36
- const pagePresentation = resolvePagePresentation(themeData, entry.data.presentation);
37
- const frameColors = resolveFrameColors({
38
- theme: themeData,
39
- pagePresentation,
40
- pageFrame: entry.data.frame,
41
- });
43
+ const siteThemeData = siteTheme.data;
44
+ const routeTheme = await getRouteTheme(currentPage.routeDirectory);
45
+ const themeData = routeTheme?.data ?? siteThemeData;
46
+ const themeLabel = routeTheme
47
+ ? `${siteDirectory || 'site'}/${routeTheme.id}`
48
+ : `${siteDirectory || 'site'}/theme.md`;
49
+ const visualTheme = resolveThemeVisualConfig(themeData, themeLabel);
50
+ const brandLabel = sitewideData.navigation?.brand ?? homePage.title;
51
+ const logoAsset = getLogoAsset();
52
+ const logoSettings = sitewideData.navigation?.logo;
53
+ const logo = logoAsset
54
+ ? {
55
+ src: logoAsset.href,
56
+ alt: logoSettings?.alt ?? brandLabel,
57
+ height: logoSettings?.height,
58
+ }
59
+ : undefined;
60
+ const pagePresentation = resolvePagePresentation(themeData);
61
+ const frameColors = resolveFrameColors({ theme: themeData });
42
62
  const sections = entry.data.sections;
43
63
  const siteHtml = entry.rendered?.html ?? '';
44
- const resolvedSections = getSectionsContent(siteHtml, rawMarkdown, sections, currentPage, pagePresentation.inlineStyles);
64
+ const resolvedSections = await getSectionsContent(siteHtml, rawMarkdown, sections, currentPage);
45
65
  const today = getToday();
66
+ const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
46
67
  const visibleSections = resolvedSections.filter((section) => isVisibleToday(section.visible, today));
47
68
 
69
+ const getPageContent = async (page: SitePage) => {
70
+ if (page.pathname === currentPage.pathname) {
71
+ return visibleSections.map(({ id, title }) => ({ id, title }));
72
+ }
73
+
74
+ const pageSections = getSectionNavigation(page.entry.rendered?.html ?? '', page.entry.data.sections);
75
+
76
+ return pageSections.filter((section) => isVisibleToday(page.entry.data.sections[section.id]?.visible, today));
77
+ };
78
+
79
+ const pageNavigation = await Promise.all(navigationPages.map(async (page) => ({
80
+ page,
81
+ sections: await getPageContent(page),
82
+ })));
83
+
48
84
  if (visibleSections.length === 0) {
49
85
  throw new Error(`No sections are visible for ${today}. Check sections.<section-id>.visible in ${siteContentLabel}.`);
50
86
  }
@@ -53,22 +89,28 @@ if (visibleSections.length === 0) {
53
89
  <BaseLayout
54
90
  title={entry.data.title}
55
91
  description={entry.data.description}
92
+ presentation={pagePresentation}
93
+ visualTheme={visualTheme}
56
94
  frameColors={frameColors}
57
95
  pathname={currentPage.pathname}
96
+ footer={sitewideData.footer}
58
97
  >
59
98
  <SiteNavigation
60
99
  brandLabel={brandLabel}
100
+ logo={logo}
61
101
  currentPage={currentPage}
62
- pages={navigationPages}
63
- sections={visibleSections}
102
+ pageNavigation={pageNavigation}
64
103
  frameColors={frameColors}
65
104
  />
105
+ <SiteBanners banners={siteBanners} />
66
106
 
67
107
  <main id="main-content" class="site-content">
68
108
  {visibleSections.map((section, index) => (
69
109
  <SiteSection
70
110
  section={section}
71
111
  pagePresentation={pagePresentation}
112
+ visualTheme={visualTheme}
113
+ sectionIndex={index}
72
114
  headingLevel={index === 0 ? 1 : 2}
73
115
  priorityGalleryImage={index === 0}
74
116
  />
@@ -1,6 +1,6 @@
1
1
  ---
2
- import type { CollectionEntry } from 'astro:content';
3
- import { resolveTypographyOverride } from '../../scripts/lib/typography.mjs';
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { resolveSectionSurface } from '../../scripts/lib/presentation.mjs';
4
4
  import CardList from './CardList.astro';
5
5
  import GalleryGrid from './GalleryGrid.astro';
6
6
  import type { ResolvedSection } from '../lib/sectionContent';
@@ -9,31 +9,44 @@ type ResponsiveValue<T> = { desktop: T; mobile: T };
9
9
  type ResponsiveOverride<T> = { desktop?: T; mobile?: T };
10
10
  type TextAlign = 'left' | 'center' | 'right';
11
11
  type TextSize = 'small' | 'medium' | 'large' | 'xlarge';
12
- type SectionPresentationOverride = {
13
- backgroundColor?: string;
14
- textColor?: string;
15
- typography?: NonNullable<ResolvedSection['presentation']>['typography'];
16
- };
12
+ type TextWidth = 'narrow' | 'normal' | 'wide';
17
13
  type ResolvedPagePresentation = {
18
- backgroundColor: string;
19
- textColor: string;
14
+ palette: {
15
+ surfaces: Record<'base' | 'soft' | 'emphasis', { backgroundColor: string; textColor: string }>;
16
+ };
17
+ sectionSurfaces: {
18
+ mode: 'none' | 'cycle';
19
+ sequence: Array<'base' | 'soft' | 'emphasis'>;
20
+ };
20
21
  typography: {
21
22
  preset: string;
22
23
  values: any;
23
24
  };
24
25
  };
26
+ type VisualTheme = {
27
+ gallery: {
28
+ maxAvailableWidthPercent: {
29
+ desktop: number;
30
+ mobile: number;
31
+ };
32
+ };
33
+ };
25
34
 
26
35
  interface Props {
27
36
  section: ResolvedSection;
28
37
  pagePresentation: ResolvedPagePresentation;
38
+ visualTheme: VisualTheme;
39
+ sectionIndex: number;
29
40
  headingLevel: 1 | 2;
30
41
  priorityGalleryImage?: boolean;
31
42
  }
32
43
 
33
- const { section, pagePresentation, headingLevel, priorityGalleryImage = false } = Astro.props;
44
+ const { section, pagePresentation, visualTheme, sectionIndex, headingLevel, priorityGalleryImage = false } = Astro.props;
34
45
  const HeadingTag = headingLevel === 1 ? 'h1' : 'h2';
35
46
  // Sections are authored with Markdown ## headings even when the first one renders as HTML h1.
36
47
  const sectionHeadingVisualLevel = 'h2';
48
+ const firstContentBlockIsVisual = section.contentBlocks[0]?.type !== 'html'
49
+ && section.contentBlocks[0]?.type !== 'note';
37
50
  type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4';
38
51
  const headingSizeValues: Record<HeadingLevel, Record<TextSize, ResponsiveValue<string>>> = {
39
52
  h1: {
@@ -145,7 +158,6 @@ const captionSizeValues: Record<TextSize, ResponsiveValue<string>> = {
145
158
  mobile: 'clamp(1.04rem, 0.98rem + 0.18vw, 1.12rem)',
146
159
  },
147
160
  };
148
- const sectionOverride = section.presentation as SectionPresentationOverride | undefined;
149
161
  const getSizeValue = (
150
162
  sizeValues: Record<TextSize, ResponsiveValue<string>>,
151
163
  size: TextSize,
@@ -153,15 +165,17 @@ const getSizeValue = (
153
165
  const getHeadingSizeValue = (level: HeadingLevel, size: TextSize) => (
154
166
  headingSizeValues[level][size]
155
167
  );
156
- const backgroundColor = sectionOverride?.backgroundColor
157
- ?? pagePresentation.backgroundColor;
158
- const textColor = sectionOverride?.textColor
159
- ?? pagePresentation.textColor;
160
- const resolvedTypography = resolveTypographyOverride(pagePresentation.typography, sectionOverride?.typography);
161
- const typography = resolvedTypography.values;
168
+ const textWidthValues: Record<TextWidth, string> = {
169
+ narrow: 'min(60ch, var(--text-width))',
170
+ normal: 'min(72ch, var(--text-width))',
171
+ wide: 'min(72ch, var(--gallery-layout-width))',
172
+ };
173
+ const surface = resolveSectionSurface(pagePresentation, sectionIndex);
174
+ const typography = pagePresentation.typography.values;
162
175
  const heading = typography.headings[sectionHeadingVisualLevel] as {
163
176
  align: ResponsiveValue<TextAlign>;
164
177
  size: TextSize;
178
+ weight: number;
165
179
  lineHeight: number;
166
180
  spacingBefore: string;
167
181
  spacingAfter: string;
@@ -172,6 +186,7 @@ const markdownHeadings = Object.fromEntries(markdownHeadingLevels.map((level) =>
172
186
  typography.headings[level] as {
173
187
  align: ResponsiveValue<TextAlign>;
174
188
  size: TextSize;
189
+ weight: number;
175
190
  lineHeight: number;
176
191
  spacingBefore: string;
177
192
  spacingAfter: string;
@@ -188,20 +203,54 @@ const justifyByAlign: Record<TextAlign, string> = {
188
203
  center: 'center',
189
204
  right: 'flex-end',
190
205
  };
191
- const getAlignedTextWidth = (
192
- align: ResponsiveValue<TextAlign>,
193
- centeredWidth: string,
194
- ): ResponsiveValue<string> => ({
195
- desktop: align.desktop === 'center' ? centeredWidth : 'var(--gallery-layout-width)',
196
- mobile: align.mobile === 'center' ? centeredWidth : 'var(--gallery-layout-width)',
197
- });
198
- const headingWidth = getAlignedTextWidth(headingAlign, '760px');
199
- const bodyWidth = getAlignedTextWidth(bodyAlign, 'var(--text-width)');
206
+ const bodyWidthValue = textWidthValues[typography.body.width as TextWidth];
207
+ const headingWidth = {
208
+ desktop: bodyWidthValue,
209
+ mobile: bodyWidthValue,
210
+ };
211
+ const bodyWidth = {
212
+ desktop: bodyWidthValue,
213
+ mobile: bodyWidthValue,
214
+ };
215
+ const isMediaBlock = (block: ResolvedSection['contentBlocks'][number]) => (
216
+ block.type === 'image-stack'
217
+ || block.type === 'image-carousel'
218
+ || block.type === 'card-list'
219
+ );
200
220
  const hasMedia = section.contentBlocks.some((block) => (
201
221
  block.type === 'image-stack'
202
222
  || block.type === 'image-carousel'
203
223
  || (block.type === 'card-list' && block.cards.some((card) => card.image))
204
224
  ));
225
+ const hasMarginNotes = section.contentBlocks.some((block) => (
226
+ block.type === 'note'
227
+ ));
228
+ const getNoteGridRow = (blocks: ResolvedSection['contentBlocks'], blockIndex: number) => Math.max(
229
+ 1,
230
+ blocks.slice(0, blockIndex).filter((block) => block.type === 'html').length,
231
+ );
232
+ const noteLabel = projectConfig.locale.labels.note;
233
+ const contentGroups: Array<
234
+ | { type: 'reading'; blocks: ResolvedSection['contentBlocks'] }
235
+ | { type: 'media'; block: Extract<ResolvedSection['contentBlocks'][number], { type: 'image-stack' | 'image-carousel' | 'card-list' }> }
236
+ > = [];
237
+ let readingBlocks: ResolvedSection['contentBlocks'] = [];
238
+ const flushReadingBlocks = () => {
239
+ if (readingBlocks.length === 0) return;
240
+ contentGroups.push({ type: 'reading', blocks: readingBlocks });
241
+ readingBlocks = [];
242
+ };
243
+
244
+ for (const block of section.contentBlocks) {
245
+ if (isMediaBlock(block)) {
246
+ flushReadingBlocks();
247
+ contentGroups.push({ type: 'media', block });
248
+ } else {
249
+ readingBlocks = [...readingBlocks, block];
250
+ }
251
+ }
252
+
253
+ flushReadingBlocks();
205
254
  const markdownHeadingStyle = (level: 'h3' | 'h4') => {
206
255
  const headingConfig = markdownHeadings[level];
207
256
  const fontSize = getHeadingSizeValue(level, headingConfig.size);
@@ -211,18 +260,20 @@ const markdownHeadingStyle = (level: 'h3' | 'h4') => {
211
260
  `--section-markdown-${level}-align-mobile: ${headingConfig.align.mobile}`,
212
261
  `--section-markdown-${level}-font-size-desktop: ${fontSize.desktop}`,
213
262
  `--section-markdown-${level}-font-size-mobile: ${fontSize.mobile}`,
263
+ `--section-markdown-${level}-font-weight: ${headingConfig.weight}`,
214
264
  `--section-markdown-${level}-line-height: ${headingConfig.lineHeight}`,
215
265
  `--section-markdown-${level}-spacing-before: ${headingConfig.spacingBefore}`,
216
266
  `--section-markdown-${level}-spacing-after: ${headingConfig.spacingAfter}`,
217
267
  ];
218
268
  };
219
269
  const sectionStyle = [
220
- `--section-background-color: ${backgroundColor}`,
221
- `--section-text-color: ${textColor}`,
270
+ `--section-background-color: ${surface.backgroundColor}`,
271
+ `--section-text-color: ${surface.textColor}`,
222
272
  `--section-heading-align-desktop: ${headingAlign.desktop}`,
223
273
  `--section-heading-align-mobile: ${headingAlign.mobile}`,
224
274
  `--section-heading-font-size-desktop: ${headingFontSize.desktop}`,
225
275
  `--section-heading-font-size-mobile: ${headingFontSize.mobile}`,
276
+ `--section-heading-font-weight: ${heading.weight}`,
226
277
  `--section-heading-line-height: ${heading.lineHeight}`,
227
278
  `--section-heading-spacing-after: ${heading.spacingAfter}`,
228
279
  `--section-heading-width-desktop: ${headingWidth.desktop}`,
@@ -250,7 +301,13 @@ const sectionStyle = [
250
301
 
251
302
  <section
252
303
  id={section.id}
253
- class="site-section"
304
+ class:list={[
305
+ 'site-section',
306
+ {
307
+ 'site-section-first-block-is-visual': firstContentBlockIsVisual,
308
+ 'site-section-has-margin-notes': hasMarginNotes,
309
+ },
310
+ ]}
254
311
  data-section-title={section.title}
255
312
  style={sectionStyle}
256
313
  >
@@ -260,33 +317,134 @@ const sectionStyle = [
260
317
  </header>
261
318
 
262
319
  <div class:list={['section-body', { 'section-body-has-gallery': hasMedia, 'section-body-text-only': !hasMedia }]}>
263
- {section.contentBlocks.map((block, blockIndex) => {
264
- if (block.type === 'html') {
265
- return block.html && <div class="section-markdown" set:html={block.html} />;
320
+ {contentGroups.map((group, groupIndex) => {
321
+ if (group.type === 'reading') {
322
+ if (hasMarginNotes && group.blocks.some((block) => block.type === 'note')) {
323
+ return (
324
+ <div class="section-reading-layout">
325
+ {group.blocks.map((block, blockIndex) => {
326
+ if (block.type === 'html') {
327
+ return block.html && <div class="section-markdown" set:html={block.html} />;
328
+ }
329
+
330
+ const noteGridRow = getNoteGridRow(group.blocks, blockIndex);
331
+ return (
332
+ <aside
333
+ class="section-note section-note-margin"
334
+ id={block.id}
335
+ aria-label={`${noteLabel}${block.number ? ` ${block.number}` : ''}`}
336
+ role="note"
337
+ data-note-id={block.id}
338
+ style={`grid-row: ${noteGridRow};`}
339
+ >
340
+ {block.number && block.referenceId && (
341
+ <a class="section-note-number" href={`#${block.referenceId}`} aria-label={`${noteLabel} ${block.number}`}>
342
+ {block.number}
343
+ </a>
344
+ )}
345
+ <div class="section-note-content" set:html={block.html} />
346
+ </aside>
347
+ );
348
+ })}
349
+ </div>
350
+ );
351
+ }
352
+
353
+ return group.blocks.map((block, blockIndex) => {
354
+ if (block.type === 'html') {
355
+ return block.html && <div class="section-markdown" set:html={block.html} />;
356
+ }
357
+
358
+ const noteGridRow = getNoteGridRow(group.blocks, blockIndex);
359
+ return (
360
+ <aside
361
+ class="section-note section-note-margin"
362
+ id={block.id}
363
+ aria-label={`${noteLabel}${block.number ? ` ${block.number}` : ''}`}
364
+ role="note"
365
+ data-note-id={block.id}
366
+ style={`grid-row: ${noteGridRow};`}
367
+ >
368
+ {block.number && block.referenceId && (
369
+ <a class="section-note-number" href={`#${block.referenceId}`} aria-label={`${noteLabel} ${block.number}`}>
370
+ {block.number}
371
+ </a>
372
+ )}
373
+ <div class="section-note-content" set:html={block.html} />
374
+ </aside>
375
+ );
376
+ });
266
377
  }
267
378
 
379
+ const block = group.block;
268
380
  if (block.type === 'image-carousel') {
269
381
  return (
270
382
  <GalleryGrid
271
383
  title={section.title}
272
384
  items={[{ carousel: block.images }]}
273
- priorityFirstImage={priorityGalleryImage && blockIndex === 0}
385
+ maxAvailableWidthPercent={visualTheme.gallery.maxAvailableWidthPercent}
386
+ priorityFirstImage={priorityGalleryImage && groupIndex === 0}
274
387
  />
275
388
  );
276
389
  }
277
390
 
278
391
  if (block.type === 'card-list') {
279
- return <CardList cards={block.cards} layout={block.layout} flow={block.flow} size={block.size} width={block.width} />;
392
+ return <CardList cards={block.cards} maxAvailableWidthPercent={visualTheme.gallery.maxAvailableWidthPercent} layout={block.layout} flow={block.flow} size={block.size} width={block.width} />;
280
393
  }
281
394
 
282
395
  return (
283
396
  <GalleryGrid
284
397
  title={section.title}
285
398
  items={block.images}
286
- priorityFirstImage={priorityGalleryImage && blockIndex === 0}
399
+ maxAvailableWidthPercent={visualTheme.gallery.maxAvailableWidthPercent}
400
+ priorityFirstImage={priorityGalleryImage && groupIndex === 0}
287
401
  />
288
402
  );
289
403
  })}
290
404
  </div>
291
405
  </div>
292
406
  </section>
407
+
408
+ <script>
409
+ const noteParts = Array.from(document.querySelectorAll<HTMLElement>('[data-note-id]'));
410
+ const noteReferences = noteParts.filter((part) => part.matches('.section-note-ref a'));
411
+ const marginNotes = noteParts.filter((part) => part.matches('.section-note'));
412
+
413
+ const setNoteState = (noteId: string, active: boolean) => {
414
+ noteParts
415
+ .filter((part) => part.dataset.noteId === noteId)
416
+ .forEach((part) => part.classList.toggle('is-referenced', active));
417
+ };
418
+
419
+ noteParts.forEach((part) => {
420
+ const noteId = part.dataset.noteId;
421
+ if (!noteId) return;
422
+
423
+ part.addEventListener('pointerenter', () => setNoteState(noteId, true));
424
+ part.addEventListener('pointerleave', () => setNoteState(noteId, false));
425
+ part.addEventListener('focusin', () => setNoteState(noteId, true));
426
+ part.addEventListener('focusout', () => setNoteState(noteId, false));
427
+ });
428
+
429
+ const alignMarginNotes = () => {
430
+ const wideLayout = window.matchMedia('(min-width: 1101px)').matches;
431
+ if (!wideLayout) {
432
+ marginNotes.forEach((note) => note.style.removeProperty('--section-note-offset'));
433
+ return;
434
+ }
435
+
436
+ marginNotes.forEach((note) => {
437
+ const noteId = note.dataset.noteId;
438
+ const reference = noteReferences.find((part) => part.dataset.noteId === noteId);
439
+ if (!noteId || !reference) return;
440
+
441
+ note.style.removeProperty('--section-note-offset');
442
+ const offset = reference.getBoundingClientRect().top - note.getBoundingClientRect().top;
443
+ note.style.setProperty('--section-note-offset', `${Math.max(0, offset)}px`);
444
+ });
445
+ };
446
+
447
+ alignMarginNotes();
448
+ window.addEventListener('resize', alignMarginNotes);
449
+ document.fonts?.ready.then(alignMarginNotes);
450
+ </script>