@janga/norna 0.7.6 → 0.7.8

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 (139) hide show
  1. package/README.md +8 -7
  2. package/astro.config.mjs +76 -0
  3. package/bin/norna-cli.mjs +2 -2
  4. package/docs/README.md +3 -3
  5. package/docs/commands.md +7 -6
  6. package/docs/configuration.md +3 -2
  7. package/docs/content.md +202 -91
  8. package/docs/design/site-examples-structure.md +1 -1
  9. package/docs/engine-development.md +3 -3
  10. package/docs/getting-started.md +50 -8
  11. package/docs/images-and-metadata.md +117 -20
  12. package/docs/routes.md +20 -12
  13. package/docs/site-structure.md +9 -5
  14. package/docs/theme.md +12 -8
  15. package/docs/typography.md +9 -10
  16. package/examples/dog-gallery/site/content.md +38 -77
  17. package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +0 -10
  18. package/fixtures/basic/site/content.md +2 -4
  19. package/fixtures/content-model-v2/.astro/collections/site.schema.json +776 -0
  20. package/fixtures/content-model-v2/.astro/collections/theme.schema.json +668 -0
  21. package/fixtures/content-model-v2/.astro/content-assets.mjs +4 -0
  22. package/fixtures/content-model-v2/.astro/content-modules.mjs +1 -0
  23. package/fixtures/content-model-v2/.astro/content.d.ts +175 -0
  24. package/fixtures/content-model-v2/.astro/data-store.json +1 -0
  25. package/fixtures/content-model-v2/.astro/dev-local.json +8 -0
  26. package/fixtures/content-model-v2/.astro/dev.json +13 -0
  27. package/fixtures/content-model-v2/.astro/dev.log +231 -0
  28. package/fixtures/content-model-v2/.astro/settings.json +5 -0
  29. package/fixtures/content-model-v2/.astro/types.d.ts +2 -0
  30. package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  31. package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +1 -0
  32. package/fixtures/content-model-v2/dist/guide/index.html +3 -0
  33. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
  34. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
  35. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
  36. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
  37. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
  38. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
  39. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
  40. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
  41. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
  42. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
  43. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
  44. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
  45. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
  46. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
  47. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
  48. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
  49. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
  50. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
  51. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
  52. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
  53. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
  54. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
  55. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
  56. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
  57. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
  58. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
  59. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
  60. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
  61. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
  62. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
  63. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
  64. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
  65. package/fixtures/content-model-v2/dist/index.html +8 -0
  66. package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +80 -0
  67. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  68. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  69. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  70. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  71. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  72. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  73. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  74. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  75. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  76. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  77. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  78. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  79. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +1389 -0
  80. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  81. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  82. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  83. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  84. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  85. package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +3 -0
  86. package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  87. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  88. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  89. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  90. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  91. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  92. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  93. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  94. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  95. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  96. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  97. package/fixtures/content-model-v2/site/.norna/generated-images.json +210 -0
  98. package/fixtures/content-model-v2/site/config.mjs +11 -0
  99. package/fixtures/content-model-v2/site/content.md +84 -0
  100. package/fixtures/content-model-v2/site/images/card-section/adopt.svg +9 -0
  101. package/fixtures/content-model-v2/site/images/card-section/foster.svg +12 -0
  102. package/fixtures/content-model-v2/site/images/intro/detail.jpg +0 -0
  103. package/fixtures/content-model-v2/site/images/intro/duplicate.jpg +0 -0
  104. package/fixtures/content-model-v2/site/images/intro/hero.jpg +0 -0
  105. package/fixtures/content-model-v2/site/images/plain/image.jpg +0 -0
  106. package/fixtures/content-model-v2/site/images/timed/slide-one.jpg +0 -0
  107. package/fixtures/content-model-v2/site/images/timed/slide-three.jpg +0 -0
  108. package/fixtures/content-model-v2/site/images/timed/slide-two.jpg +0 -0
  109. package/fixtures/content-model-v2/site/routes/010-guide/images/intro/duplicate.jpg +0 -0
  110. package/fixtures/content-model-v2/site/routes/010-guide/route-content.md +17 -0
  111. package/fixtures/content-model-v2/site/theme.md +9 -0
  112. package/package.json +3 -2
  113. package/scripts/deploy-site.mjs +11 -4
  114. package/scripts/dev-local.mjs +6 -0
  115. package/scripts/generate-images.mjs +132 -56
  116. package/scripts/lib/image-dimensions.mjs +26 -0
  117. package/scripts/lib/norna-markdown-blocks.mjs +643 -0
  118. package/scripts/lib/site-content.mjs +31 -29
  119. package/scripts/show-typography.mjs +17 -9
  120. package/scripts/sync-content-sections.mjs +414 -412
  121. package/scripts/test-content-check.mjs +59 -45
  122. package/scripts/test-content-model-v2.mjs +1230 -0
  123. package/scripts/test-engine-commands.mjs +1 -1
  124. package/scripts/test-package-check.mjs +36 -8
  125. package/scripts/test-temporary-visibility.mjs +3 -6
  126. package/src/components/CardList.astro +77 -0
  127. package/src/components/GalleryGrid.astro +22 -12
  128. package/src/components/SiteNavigation.astro +5 -4
  129. package/src/components/SitePage.astro +9 -2
  130. package/src/components/SiteSection.astro +35 -13
  131. package/src/content.config.ts +4 -17
  132. package/src/lib/generatedImages.ts +50 -10
  133. package/src/lib/sectionContent.ts +130 -35
  134. package/src/styles/global.css +187 -0
  135. package/starters/basic/README.md +4 -2
  136. package/starters/basic/site/content.md +9 -11
  137. package/starters/project/README.md +7 -3
  138. package/starters/project/site/content.md +0 -9
  139. package/starters/project/site/routes/010-guide/route-content.md +0 -5
@@ -1,14 +1,51 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import {
4
+ extractNornaMarkdownBlocks,
5
+ splitNornaMarkdownBlockMarkers,
6
+ splitNornaRenderedCodeBlocks,
7
+ } from '../../scripts/lib/norna-markdown-blocks.mjs';
3
8
  import { applyBasePathToHtml } from './basePath';
9
+ import type { SitePage } from './sitePages';
4
10
 
5
- type SiteSection = CollectionEntry<'site'>['data']['sections'][number];
11
+ type SiteSectionMetadataMap = CollectionEntry<'site'>['data']['sections'];
12
+ type SiteSectionMetadata = SiteSectionMetadataMap[string];
6
13
  type ThemePresentation = CollectionEntry<'theme'>['data']['presentation'];
7
14
  type InlineStyles = NonNullable<NonNullable<ThemePresentation>['inlineStyles']>;
15
+ type GalleryImage = {
16
+ image: string;
17
+ src: string;
18
+ alt?: string;
19
+ caption?: string;
20
+ };
21
+ type CardListItem = {
22
+ title: string;
23
+ text?: string;
24
+ image?: string;
25
+ src?: string;
26
+ link?: string;
27
+ 'badge-text'?: string;
28
+ };
29
+ type CardListLayout = 'image-top' | 'image-left' | 'image-right';
30
+ type CardListFlow = 'grid' | 'stack';
31
+ type CardListSize = 's' | 'm' | 'l' | 'xl';
32
+ type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
33
+ type SectionContentBlock =
34
+ | { type: 'html'; html: string }
35
+ | { type: 'image-stack'; images: GalleryImage[] }
36
+ | { type: 'image-carousel'; images: GalleryImage[] }
37
+ | { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width: CardListWidth; cards: CardListItem[] };
38
+ export type ResolvedSection = SiteSectionMetadata & {
39
+ id: string;
40
+ title: string;
41
+ contentBlocks: SectionContentBlock[];
42
+ };
8
43
 
9
44
  const headingRegex = /<h2\b([^>]*)>([\s\S]*?)<\/h2>/gi;
10
45
  const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
11
46
  const inlineStyleReferenceRegex = /\[([^\]<]+)\]\{\.([a-z][a-z0-9-]*)\}/g;
47
+ const markdownH2Regex = /^##\s+.*$/gm;
48
+ const imageProvenanceCommentRegex = /<!--\s*norna-image-provenance:[\s\S]*?-->/gi;
12
49
 
13
50
  const stripTags = (html: string) => html.replace(/<[^>]*>/g, '');
14
51
 
@@ -58,18 +95,94 @@ const applyInlineStyles = (html: string, inlineStyles: InlineStyles | undefined)
58
95
  return `<span class="inline-style inline-style-${styleName}" style="--inline-style-color: ${style.color}">${text}</span>`;
59
96
  });
60
97
 
98
+ const stripImageProvenanceComments = (html: string) => html.replace(imageProvenanceCommentRegex, '');
99
+
61
100
  const prepareContentHtml = (html: string, inlineStyles: InlineStyles | undefined) =>
62
- applyBasePathToHtml(projectConfig.site.basePath, applyInlineStyles(html, inlineStyles));
101
+ applyBasePathToHtml(
102
+ projectConfig.site.basePath,
103
+ stripImageProvenanceComments(applyInlineStyles(html, inlineStyles)),
104
+ );
105
+
106
+ const getRawMarkdownSections = (markdown: string) => {
107
+ const matches = Array.from(markdown.matchAll(markdownH2Regex));
108
+ const sections = new Map<string, string>();
109
+
110
+ for (let index = 0; index < matches.length; index += 1) {
111
+ const match = matches[index];
112
+ const heading = match[0] ?? '';
113
+ const id = heading.match(explicitHeadingIdRegex)?.[1];
114
+ if (!id) continue;
115
+
116
+ const start = match.index ?? 0;
117
+ const next = matches[index + 1];
118
+ const end = next?.index ?? markdown.length;
119
+ sections.set(id, markdown.slice(start, end));
120
+ }
121
+
122
+ return sections;
123
+ };
124
+
125
+ const getImageSourceKey = (page: SitePage, sectionId: string, image: string) => (
126
+ page.routeDirectory
127
+ ? `routes/${page.routeDirectory}/images/${sectionId}/${image}`
128
+ : `images/${sectionId}/${image}`
129
+ );
130
+
131
+ const resolveContentBlocks = (
132
+ html: string,
133
+ rawMarkdown: string,
134
+ page: SitePage,
135
+ sectionId: string,
136
+ inlineStyles: InlineStyles | undefined,
137
+ ) => {
138
+ const rawBlocks = extractNornaMarkdownBlocks(rawMarkdown);
139
+ const splitBlocks = rawBlocks.length > 0
140
+ ? splitNornaRenderedCodeBlocks(html, rawBlocks)
141
+ : splitNornaMarkdownBlockMarkers(html);
142
+
143
+ return splitBlocks.map((block): SectionContentBlock => {
144
+ if (block.type === 'html') {
145
+ return {
146
+ type: 'html',
147
+ html: prepareContentHtml(block.html, inlineStyles),
148
+ };
149
+ }
150
+
151
+ return {
152
+ type: block.type,
153
+ ...(block.type === 'card-list'
154
+ ? {
155
+ layout: block.layout,
156
+ flow: block.flow,
157
+ size: block.size,
158
+ width: block.width,
159
+ cards: block.cards.map((card: CardListItem) => ({
160
+ ...card,
161
+ ...(card.image ? { src: getImageSourceKey(page, sectionId, card.image) } : {}),
162
+ })),
163
+ }
164
+ : {
165
+ images: block.images.map((image: { image: string; alt?: string; caption?: string }) => ({
166
+ ...image,
167
+ src: getImageSourceKey(page, sectionId, image.image),
168
+ })),
169
+ }),
170
+ };
171
+ });
172
+ };
63
173
 
64
174
  export const getSectionsContent = (
65
175
  html: string,
66
- sections: SiteSection[],
176
+ rawMarkdown: string,
177
+ sectionMetadata: SiteSectionMetadataMap,
178
+ page: SitePage,
67
179
  inlineStyles?: InlineStyles,
68
180
  ) => {
69
181
  const matches = Array.from(html.matchAll(headingRegex));
70
- const contentById = new Map<string, { title: string; contentHtml: string }>();
71
- const sectionIds = new Set(sections.map((section) => section.id));
72
- const markdownSectionIds: string[] = [];
182
+ const rawSections = getRawMarkdownSections(rawMarkdown);
183
+ const metadataIds = new Set(Object.keys(sectionMetadata));
184
+ const sections: ResolvedSection[] = [];
185
+ const sectionIds = new Set<string>();
73
186
 
74
187
  for (let index = 0; index < matches.length; index += 1) {
75
188
  const match = matches[index];
@@ -84,47 +197,29 @@ export const getSectionsContent = (
84
197
  const title = getHeadingTitle(headingHtml);
85
198
 
86
199
  if (!explicitId) {
87
- console.warn(`Markdown section is missing an explicit heading id: "${title}". Write for example "## ${title} {#${id}}".`);
200
+ throw new Error(`Section heading "${title}" is missing an explicit id. Write it as: ## ${title} {#${id}}`);
88
201
  }
89
202
 
90
- if (contentById.has(id)) {
203
+ if (sectionIds.has(id)) {
91
204
  throw new Error(`Duplicate Markdown section heading id: ${id}`);
92
205
  }
93
206
 
94
- contentById.set(id, {
207
+ sectionIds.add(id);
208
+ sections.push({
209
+ ...(sectionMetadata[id] ?? {}),
210
+ id,
95
211
  title,
96
- contentHtml: prepareContentHtml(content, inlineStyles),
212
+ contentBlocks: resolveContentBlocks(content, rawSections.get(id) ?? '', page, id, inlineStyles),
97
213
  });
98
- markdownSectionIds.push(id);
99
214
  }
100
215
 
101
- for (const id of contentById.keys()) {
216
+ for (const id of metadataIds) {
102
217
  if (!sectionIds.has(id)) {
103
- console.warn(`Markdown section exists but is not used in frontmatter: ${id}`);
104
- }
105
- }
106
-
107
- const orderedMarkdownSectionIds = markdownSectionIds.filter((id) => sectionIds.has(id));
108
- const frontmatterSectionIds = sections.map((section) => section.id);
109
- const hasOrderMismatch = frontmatterSectionIds.some((id, index) => id !== orderedMarkdownSectionIds[index]);
110
-
111
- if (hasOrderMismatch) {
112
- console.warn('Markdown section order differs from frontmatter. Run npm run content:sync to sort Markdown according to frontmatter.');
113
- }
114
-
115
- return sections.map((section) => {
116
- const content = contentById.get(section.id);
117
-
118
- if (!content) {
119
218
  throw new Error(
120
- `Cannot find heading for "${section.id}". Each frontmatter section must have a matching level 2 Markdown heading, for example: ## Heading {#${section.id}}`,
219
+ `Section metadata "${id}" does not match any Markdown section. Add "## Heading {#${id}}" or remove sections.${id}.`,
121
220
  );
122
221
  }
222
+ }
123
223
 
124
- return {
125
- ...section,
126
- title: content.title,
127
- contentHtml: content.contentHtml,
128
- };
129
- });
224
+ return sections;
130
225
  };
@@ -458,6 +458,182 @@ ul {
458
458
  margin-top: 0.35rem;
459
459
  }
460
460
 
461
+ .card-list {
462
+ display: grid;
463
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
464
+ gap: var(--card-list-gap, clamp(0.9rem, 2vw, 1.25rem));
465
+ width: min(100%, var(--card-list-width, var(--gallery-layout-width)));
466
+ margin: 0 auto var(--space-section-body-to-gallery);
467
+ text-align: left;
468
+ --card-list-width: var(--gallery-layout-width);
469
+ --card-list-gap: clamp(0.9rem, 2vw, 1.25rem);
470
+ --card-list-item-gap: 0.85rem;
471
+ --card-list-padding: clamp(0.9rem, 2vw, 1.15rem);
472
+ --card-list-media-width: 32%;
473
+ --card-list-media-min-width: 7rem;
474
+ --card-list-top-media-width: 20rem;
475
+ --card-list-title-scale: 1;
476
+ --card-list-text-scale: 1;
477
+ }
478
+
479
+ .card-list-width-text {
480
+ --card-list-width: var(--text-width);
481
+ }
482
+
483
+ .card-list-width-narrow {
484
+ --card-list-width: min(48rem, var(--gallery-layout-width));
485
+ }
486
+
487
+ .card-list-width-wide {
488
+ --card-list-width: var(--page-width);
489
+ }
490
+
491
+ .card-list-size-s {
492
+ --card-list-gap: 0.75rem;
493
+ --card-list-item-gap: 0.65rem;
494
+ --card-list-padding: 0.8rem;
495
+ --card-list-media-width: 26%;
496
+ --card-list-media-min-width: 5.5rem;
497
+ --card-list-top-media-width: 14rem;
498
+ --card-list-title-scale: 0.92;
499
+ --card-list-text-scale: 0.92;
500
+ }
501
+
502
+ .card-list-size-l {
503
+ --card-list-gap: clamp(1rem, 2.4vw, 1.5rem);
504
+ --card-list-item-gap: 1rem;
505
+ --card-list-padding: clamp(1.1rem, 2.4vw, 1.45rem);
506
+ --card-list-media-width: 36%;
507
+ --card-list-media-min-width: 8.5rem;
508
+ --card-list-top-media-width: 24rem;
509
+ --card-list-title-scale: 1.08;
510
+ --card-list-text-scale: 1.02;
511
+ }
512
+
513
+ .card-list-size-xl {
514
+ --card-list-gap: clamp(1.15rem, 2.8vw, 1.8rem);
515
+ --card-list-item-gap: 1.1rem;
516
+ --card-list-padding: clamp(1.25rem, 3vw, 1.75rem);
517
+ --card-list-media-width: 40%;
518
+ --card-list-media-min-width: 10rem;
519
+ --card-list-top-media-width: 28rem;
520
+ --card-list-title-scale: 1.18;
521
+ --card-list-text-scale: 1.06;
522
+ }
523
+
524
+ .card-list-flow-stack {
525
+ grid-template-columns: 1fr;
526
+ }
527
+
528
+ .card-list-item {
529
+ display: grid;
530
+ gap: var(--card-list-item-gap);
531
+ align-content: start;
532
+ margin: 0;
533
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
534
+ border-radius: 8px;
535
+ padding: var(--card-list-padding);
536
+ background: color-mix(in srgb, var(--section-background-color, var(--color-page)) 92%, currentColor 8%);
537
+ color: inherit;
538
+ }
539
+
540
+ .card-list-layout-image-left .card-list-item-has-image,
541
+ .card-list-layout-image-right .card-list-item-has-image {
542
+ grid-template-columns: minmax(var(--card-list-media-min-width), var(--card-list-media-width)) minmax(0, 1fr);
543
+ align-items: center;
544
+ }
545
+
546
+ .card-list-layout-image-right .card-list-item-has-image {
547
+ grid-template-columns: minmax(0, 1fr) minmax(var(--card-list-media-min-width), var(--card-list-media-width));
548
+ }
549
+
550
+ .card-list-layout-image-left .card-list-item-linked,
551
+ .card-list-layout-image-right .card-list-item-linked {
552
+ grid-template-columns: 1fr;
553
+ }
554
+
555
+ .card-list-layout-image-right .card-list-image-frame {
556
+ order: 2;
557
+ }
558
+
559
+ .card-list-link {
560
+ display: grid;
561
+ gap: var(--card-list-item-gap);
562
+ width: 100%;
563
+ color: inherit;
564
+ text-decoration: none;
565
+ }
566
+
567
+ .card-list-layout-image-left .card-list-item-has-image .card-list-link,
568
+ .card-list-layout-image-right .card-list-item-has-image .card-list-link {
569
+ grid-template-columns: minmax(var(--card-list-media-min-width), var(--card-list-media-width)) minmax(0, 1fr);
570
+ align-items: center;
571
+ }
572
+
573
+ .card-list-layout-image-right .card-list-item-has-image .card-list-link {
574
+ grid-template-columns: minmax(0, 1fr) minmax(var(--card-list-media-min-width), var(--card-list-media-width));
575
+ }
576
+
577
+ .card-list-link:hover .card-list-title,
578
+ .card-list-link:focus-visible .card-list-title {
579
+ text-decoration: underline;
580
+ text-underline-offset: 0.18em;
581
+ }
582
+
583
+ .card-list-image-frame {
584
+ display: grid;
585
+ width: 100%;
586
+ aspect-ratio: 16 / 10;
587
+ place-items: center;
588
+ overflow: hidden;
589
+ }
590
+
591
+ .card-list-flow-stack.card-list-layout-image-top .card-list-image-frame {
592
+ width: min(100%, var(--card-list-top-media-width));
593
+ }
594
+
595
+ .card-list-image {
596
+ display: block;
597
+ width: 100%;
598
+ height: 100%;
599
+ object-fit: contain;
600
+ }
601
+
602
+ .card-list-content {
603
+ display: grid;
604
+ gap: 0.4rem;
605
+ }
606
+
607
+ .card-list-badge {
608
+ justify-self: start;
609
+ margin: 0 0 0.15rem;
610
+ border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
611
+ border-radius: 999px;
612
+ padding: 0.18rem 0.48rem;
613
+ background: color-mix(in srgb, currentColor 9%, transparent);
614
+ color: inherit;
615
+ font-size: calc(var(--section-body-font-size-desktop, 1rem) * 0.72);
616
+ font-weight: 700;
617
+ letter-spacing: 0;
618
+ line-height: 1.2;
619
+ }
620
+
621
+ .card-list-title {
622
+ margin: 0;
623
+ color: inherit;
624
+ font-size: calc(var(--section-markdown-h3-font-size-desktop, 1.1rem) * var(--card-list-title-scale));
625
+ font-weight: 700;
626
+ letter-spacing: 0;
627
+ line-height: var(--section-markdown-h3-line-height, 1.35);
628
+ text-transform: none;
629
+ }
630
+
631
+ .card-list-content p:not(.card-list-badge) {
632
+ margin: 0;
633
+ font-size: calc(var(--section-body-font-size-desktop, 1rem) * var(--card-list-text-scale));
634
+ line-height: var(--section-body-line-height, 1.5);
635
+ }
636
+
461
637
  .gallery {
462
638
  display: grid;
463
639
  gap: var(--space-gallery-item);
@@ -858,6 +1034,17 @@ ul {
858
1034
  text-align: var(--section-markdown-h4-align-mobile, var(--section-markdown-h4-align-desktop, left));
859
1035
  }
860
1036
 
1037
+ .card-list-layout-image-left .card-list-item-has-image,
1038
+ .card-list-layout-image-right .card-list-item-has-image,
1039
+ .card-list-layout-image-left .card-list-item-has-image .card-list-link,
1040
+ .card-list-layout-image-right .card-list-item-has-image .card-list-link {
1041
+ grid-template-columns: 1fr;
1042
+ }
1043
+
1044
+ .card-list-layout-image-right .card-list-image-frame {
1045
+ order: 0;
1046
+ }
1047
+
861
1048
  .section-markdown ul,
862
1049
  .section-markdown ol {
863
1050
  display: block;
@@ -17,8 +17,10 @@ npm run dev -- --kill
17
17
 
18
18
  Edit site-wide visual defaults in `site/theme.md`, homepage content and section
19
19
  overrides in `site/content.md`, technical settings such as URL and locale
20
- labels in `site/config.mjs`, source images under
21
- `site/images/<section-id>/`, and static public files under `site/public/`.
20
+ labels in `site/config.mjs`, source images under `site/images/<section-id>/`,
21
+ and static public files under `site/public/`. The image folder name should
22
+ match the section id in Markdown, for example `## Work {#work}` uses
23
+ `site/images/work/`.
22
24
 
23
25
  Use `site.basePath: '/'` for a custom domain or root-hosted site. For a GitHub
24
26
  Pages project site without a custom domain, set both values:
@@ -1,16 +1,6 @@
1
1
  ---
2
2
  title: Example Site
3
3
  description: Minimal norna starter site.
4
- sections:
5
- - id: intro
6
- presentation:
7
- typography:
8
- preset: statement
9
- - id: work
10
- - id: lorem1
11
- - id: lorem2
12
- - id: lorem3
13
-
14
4
  ---
15
5
 
16
6
  ## Intro {#intro}
@@ -19,7 +9,15 @@ routes defined, the navigation menu will be a sticky bar with anchor links for
19
9
  ease of use on phones etc. [Highlighted inline text]{.highlight} when a short
20
10
  phrase needs emphasis
21
11
  ## Work {#work}
22
- Add image rows in frontmatter and matching source images under images/work/.
12
+ Add Norna managed media blocks in Markdown where the media should appear. Put
13
+ matching source images under `images/work/`.
14
+
15
+ ````
16
+ ```norna-image-stack
17
+ - image: example.jpg
18
+ caption: Replace this with a real image and caption.
19
+ ```
20
+ ````
23
21
  ## What is Lorem Ipsum? {#lorem1}
24
22
  Lorem Ipsum is simply dummy text of the printing and typesetting
25
23
  industry. Lorem Ipsum has been the industry's standard dummy text ever
@@ -23,7 +23,8 @@ npm run dev -- --kill
23
23
  - `site/theme.md`: site-wide visual settings such as brand text, layout
24
24
  density, typography rhythm, colors, frame colors, and inline text styles.
25
25
  - `site/content.md`: homepage title, sections, placeholders, project summary,
26
- links, install command, example usage, benefits, next steps, and license.
26
+ links, install command, example usage, benefits, next steps, managed media
27
+ blocks, and license.
27
28
  - `site/routes/010-guide/route-content.md`: short secondary page with realistic
28
29
  project guide content.
29
30
  - `site/public/robots.txt`: static public file copied into the built site.
@@ -40,15 +41,18 @@ GitHub Pages workflow uses `npm ci`.
40
41
  delete the route if the homepage is enough.
41
42
  3. Edit `site/theme.md` for brand name, layout density, typography rhythm,
42
43
  colors, frame colors, and inline text styles.
43
- 4. Edit `site/config.mjs` for public URL, GitHub repository, footer text, and
44
+ 4. Put any local source images in the image folder for the Markdown section
45
+ that references them, for example `site/images/intro/`.
46
+ 5. Edit `site/config.mjs` for public URL, GitHub repository, footer text, and
44
47
  deploy workflow name.
45
- 5. Update `package.json` with the site's package name and keep
48
+ 6. Update `package.json` with the site's package name and keep
46
49
  `package-lock.json` committed.
47
50
 
48
51
  ## Common Commands
49
52
 
50
53
  ```sh
51
54
  npm run norna:check
55
+ npm run norna:sync
52
56
  npm run norna:typography:show
53
57
  npm run build
54
58
  ```
@@ -3,15 +3,6 @@ title: Your project
3
3
  description: A compact project site starter for an open source developer tool.
4
4
  navigation:
5
5
  label: Home
6
- sections:
7
- - id: intro
8
- - id: links
9
- - id: install
10
- - id: example
11
- - id: why
12
- - id: good-fit
13
- - id: next
14
- - id: license
15
6
  ---
16
7
 
17
8
  ## `<Your project> - <short value proposition>` {#intro}
@@ -3,11 +3,6 @@ title: Guide
3
3
  description: Adapt this compact guide for normal project use.
4
4
  navigation:
5
5
  label: Guide
6
- sections:
7
- - id: quickstart
8
- - id: configuration
9
- - id: usage
10
- - id: topic
11
6
  ---
12
7
 
13
8
  ## Quickstart {#quickstart}