@janga/norna 0.7.6 → 0.7.7

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 +5 -5
  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 +6 -6
  6. package/docs/configuration.md +3 -2
  7. package/docs/content.md +152 -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 +4 -3
  11. package/docs/images-and-metadata.md +112 -20
  12. package/docs/routes.md +9 -12
  13. package/docs/site-structure.md +4 -3
  14. package/docs/theme.md +8 -7
  15. package/docs/typography.md +5 -5
  16. package/examples/dog-gallery/site/content.md +41 -45
  17. package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +1 -3
  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 +280 -411
  121. package/scripts/test-content-check.mjs +59 -45
  122. package/scripts/test-content-model-v2.mjs +998 -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/site/content.md +10 -6
  136. package/starters/project/site/content.md +0 -9
  137. 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;
@@ -2,14 +2,10 @@
2
2
  title: Example Site
3
3
  description: Minimal norna starter site.
4
4
  sections:
5
- - id: intro
5
+ intro:
6
6
  presentation:
7
7
  typography:
8
8
  preset: statement
9
- - id: work
10
- - id: lorem1
11
- - id: lorem2
12
- - id: lorem3
13
9
 
14
10
  ---
15
11
 
@@ -19,7 +15,15 @@ routes defined, the navigation menu will be a sticky bar with anchor links for
19
15
  ease of use on phones etc. [Highlighted inline text]{.highlight} when a short
20
16
  phrase needs emphasis
21
17
  ## Work {#work}
22
- Add image rows in frontmatter and matching source images under images/work/.
18
+ Add Norna image blocks in Markdown and matching source images under
19
+ `images/work/`.
20
+
21
+ ````
22
+ ```norna-image-stack
23
+ - image: example.jpg
24
+ caption: Replace this with a real image and caption.
25
+ ```
26
+ ````
23
27
  ## What is Lorem Ipsum? {#lorem1}
24
28
  Lorem Ipsum is simply dummy text of the printing and typesetting
25
29
  industry. Lorem Ipsum has been the industry's standard dummy text ever
@@ -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}