@janga/norna 0.7.3 → 0.7.4

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 (87) hide show
  1. package/docs/README.md +3 -3
  2. package/docs/commands.md +2 -2
  3. package/docs/configuration.md +2 -1
  4. package/docs/design/command-organization.md +4 -3
  5. package/docs/design/site-examples-structure.md +4 -3
  6. package/docs/getting-started.md +8 -7
  7. package/docs/theme.md +27 -43
  8. package/docs/typography.md +47 -31
  9. package/examples/dog-gallery/site/content.md +1 -1
  10. package/examples/dog-gallery/site/theme.md +12 -14
  11. package/package.json +1 -1
  12. package/scripts/check-config.mjs +1 -0
  13. package/scripts/lib/project-config.mjs +87 -32
  14. package/scripts/lib/site-content.mjs +4 -4
  15. package/scripts/lib/typography.mjs +79 -39
  16. package/scripts/show-typography.mjs +75 -11
  17. package/scripts/test-engine-commands.mjs +18 -0
  18. package/scripts/test-package-check.mjs +19 -3
  19. package/src/components/SiteSection.astro +82 -20
  20. package/src/content.config.ts +11 -9
  21. package/src/layouts/BaseLayout.astro +0 -6
  22. package/src/styles/global.css +44 -17
  23. package/starters/basic/README.md +4 -3
  24. package/starters/basic/site/theme.md +19 -46
  25. package/starters/project/README.md +7 -7
  26. package/starters/project/package-lock.json +4 -4
  27. package/starters/project/package.json +1 -1
  28. package/starters/project/site/content.md +86 -46
  29. package/starters/project/site/routes/guide/route-content.md +52 -25
  30. package/starters/project/site/theme.md +29 -1
  31. package/examples/dog-gallery/.astro/collections/site.schema.json +0 -802
  32. package/examples/dog-gallery/.astro/collections/theme.schema.json +0 -714
  33. package/examples/dog-gallery/.astro/content-assets.mjs +0 -4
  34. package/examples/dog-gallery/.astro/content-modules.mjs +0 -1
  35. package/examples/dog-gallery/.astro/content.d.ts +0 -175
  36. package/examples/dog-gallery/.astro/data-store.json +0 -1
  37. package/examples/dog-gallery/.astro/dev-local.json +0 -8
  38. package/examples/dog-gallery/.astro/dev.json +0 -13
  39. package/examples/dog-gallery/.astro/dev.log +0 -49
  40. package/examples/dog-gallery/.astro/settings.json +0 -5
  41. package/examples/dog-gallery/.astro/types.d.ts +0 -2
  42. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +0 -1
  43. package/examples/dog-gallery/dist/_astro/SitePage.BFHZm5UG.css +0 -1
  44. package/examples/dog-gallery/dist/dog-care/index.html +0 -15
  45. package/examples/dog-gallery/dist/favicon.svg +0 -7
  46. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  47. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  48. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  75. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  76. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  77. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  78. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  79. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  80. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  81. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  82. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  83. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  84. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  85. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  86. package/examples/dog-gallery/dist/index.html +0 -43
  87. package/examples/dog-gallery/dist/robots.txt +0 -2
@@ -30,6 +30,7 @@ const knownNestedFrontmatterKeys = new Set([
30
30
  'carousel',
31
31
  'color',
32
32
  'colors',
33
+ 'density',
33
34
  'desktop',
34
35
  'fontFamily',
35
36
  'from',
@@ -59,13 +60,12 @@ const knownNestedFrontmatterKeys = new Set([
59
60
  'pageWidth',
60
61
  'preset',
61
62
  'presentation',
63
+ 'rhythm',
62
64
  'sections',
63
65
  'sectionGap',
64
- 'sectionHeadingToBody',
65
66
  'size',
66
- 'spacing',
67
- 'subheadingRuleTop',
68
- 'subheadingTop',
67
+ 'spacingAfter',
68
+ 'spacingBefore',
69
69
  'textColor',
70
70
  'theme',
71
71
  'typography',
@@ -5,171 +5,199 @@ export const typographyPresetNames = [
5
5
  'statement',
6
6
  ];
7
7
 
8
+ export const typographyRhythmNames = [
9
+ 'compact',
10
+ 'normal',
11
+ 'airy',
12
+ ];
13
+
8
14
  export const typographyPresets = {
9
15
  'quiet-gallery': {
10
16
  headings: {
11
17
  h1: {
12
18
  align: { desktop: 'left', mobile: 'left' },
13
- size: 'large',
19
+ size: 'medium',
14
20
  lineHeight: 1.04,
15
- spacing: '0.55em',
16
21
  },
17
22
  h2: {
18
23
  align: { desktop: 'left', mobile: 'left' },
19
24
  size: 'medium',
20
25
  lineHeight: 1.08,
21
- spacing: '0.65em',
22
26
  },
23
27
  h3: {
24
28
  align: { desktop: 'left', mobile: 'left' },
25
- size: 'small',
29
+ size: 'medium',
26
30
  lineHeight: 1.35,
27
- spacing: '0.9rem',
28
31
  },
29
32
  h4: {
30
33
  align: { desktop: 'left', mobile: 'left' },
31
34
  size: 'medium',
32
35
  lineHeight: 1.5,
33
- spacing: '0.85rem',
34
36
  },
35
37
  },
36
38
  body: {
37
39
  align: { desktop: 'left', mobile: 'left' },
38
40
  size: 'medium',
39
41
  lineHeight: 1.5,
40
- paragraphSpacing: '0.85em',
41
42
  },
42
43
  caption: {
43
44
  align: { desktop: 'center', mobile: 'center' },
44
- size: 'small',
45
+ size: 'medium',
45
46
  lineHeight: 1.35,
46
- spacing: '0.5em',
47
47
  },
48
48
  },
49
49
  'compact-gallery': {
50
50
  headings: {
51
51
  h1: {
52
52
  align: { desktop: 'left', mobile: 'left' },
53
- size: 'large',
53
+ size: 'medium',
54
54
  lineHeight: 1.04,
55
- spacing: '0.45em',
56
55
  },
57
56
  h2: {
58
57
  align: { desktop: 'left', mobile: 'left' },
59
58
  size: 'medium',
60
59
  lineHeight: 1.08,
61
- spacing: '0.45em',
62
60
  },
63
61
  h3: {
64
62
  align: { desktop: 'left', mobile: 'left' },
65
- size: 'small',
63
+ size: 'medium',
66
64
  lineHeight: 1.3,
67
- spacing: '0.75rem',
68
65
  },
69
66
  h4: {
70
67
  align: { desktop: 'left', mobile: 'left' },
71
68
  size: 'medium',
72
69
  lineHeight: 1.42,
73
- spacing: '0.6rem',
74
70
  },
75
71
  },
76
72
  body: {
77
73
  align: { desktop: 'left', mobile: 'left' },
78
74
  size: 'medium',
79
75
  lineHeight: 1.42,
80
- paragraphSpacing: '0.6em',
81
76
  },
82
77
  caption: {
83
78
  align: { desktop: 'center', mobile: 'center' },
84
- size: 'small',
79
+ size: 'medium',
85
80
  lineHeight: 1.25,
86
- spacing: '0.35em',
87
81
  },
88
82
  },
89
83
  'text-forward': {
90
84
  headings: {
91
85
  h1: {
92
86
  align: { desktop: 'left', mobile: 'left' },
93
- size: 'large',
87
+ size: 'medium',
94
88
  lineHeight: 1.06,
95
- spacing: '0.65em',
96
89
  },
97
90
  h2: {
98
91
  align: { desktop: 'left', mobile: 'left' },
99
92
  size: 'medium',
100
93
  lineHeight: 1.12,
101
- spacing: '0.8em',
102
94
  },
103
95
  h3: {
104
96
  align: { desktop: 'left', mobile: 'left' },
105
- size: 'small',
97
+ size: 'medium',
106
98
  lineHeight: 1.35,
107
- spacing: '1rem',
108
99
  },
109
100
  h4: {
110
101
  align: { desktop: 'left', mobile: 'left' },
111
102
  size: 'medium',
112
103
  lineHeight: 1.55,
113
- spacing: '0.85rem',
114
104
  },
115
105
  },
116
106
  body: {
117
107
  align: { desktop: 'left', mobile: 'left' },
118
108
  size: 'medium',
119
109
  lineHeight: 1.62,
120
- paragraphSpacing: '1em',
121
110
  },
122
111
  caption: {
123
112
  align: { desktop: 'left', mobile: 'left' },
124
- size: 'small',
113
+ size: 'medium',
125
114
  lineHeight: 1.4,
126
- spacing: '0.6em',
127
115
  },
128
116
  },
129
117
  statement: {
130
118
  headings: {
131
119
  h1: {
132
120
  align: { desktop: 'left', mobile: 'left' },
133
- size: 'xlarge',
121
+ size: 'medium',
134
122
  lineHeight: 1.02,
135
- spacing: '0.45em',
136
123
  },
137
124
  h2: {
138
125
  align: { desktop: 'left', mobile: 'left' },
139
- size: 'large',
126
+ size: 'medium',
140
127
  lineHeight: 1.04,
141
- spacing: '0.5em',
142
128
  },
143
129
  h3: {
144
130
  align: { desktop: 'left', mobile: 'left' },
145
131
  size: 'medium',
146
132
  lineHeight: 1.18,
147
- spacing: '0.85rem',
148
133
  },
149
134
  h4: {
150
135
  align: { desktop: 'left', mobile: 'left' },
151
136
  size: 'medium',
152
137
  lineHeight: 1.42,
153
- spacing: '0.75rem',
154
138
  },
155
139
  },
156
140
  body: {
157
141
  align: { desktop: 'left', mobile: 'left' },
158
142
  size: 'medium',
159
143
  lineHeight: 1.42,
160
- paragraphSpacing: '0.75em',
161
144
  },
162
145
  caption: {
163
146
  align: { desktop: 'center', mobile: 'center' },
164
- size: 'small',
147
+ size: 'medium',
165
148
  lineHeight: 1.3,
166
- spacing: '0.45em',
149
+ },
150
+ },
151
+ };
152
+
153
+ export const typographyRhythms = {
154
+ compact: {
155
+ headings: {
156
+ h1: { spacingBefore: '0', spacingAfter: '0.4em' },
157
+ h2: { spacingBefore: '0', spacingAfter: '0.45em' },
158
+ h3: { spacingBefore: '1.2em', spacingAfter: '0.4em' },
159
+ h4: { spacingBefore: '0.9em', spacingAfter: '0.35em' },
160
+ },
161
+ body: {
162
+ paragraphSpacing: '0.65em',
163
+ },
164
+ caption: {
165
+ spacingBefore: '0.35em',
166
+ },
167
+ },
168
+ normal: {
169
+ headings: {
170
+ h1: { spacingBefore: '0', spacingAfter: '0.5em' },
171
+ h2: { spacingBefore: '0', spacingAfter: '0.55em' },
172
+ h3: { spacingBefore: '1.5em', spacingAfter: '0.5em' },
173
+ h4: { spacingBefore: '1.1em', spacingAfter: '0.4em' },
174
+ },
175
+ body: {
176
+ paragraphSpacing: '0.85em',
177
+ },
178
+ caption: {
179
+ spacingBefore: '0.5em',
180
+ },
181
+ },
182
+ airy: {
183
+ headings: {
184
+ h1: { spacingBefore: '0', spacingAfter: '0.65em' },
185
+ h2: { spacingBefore: '0', spacingAfter: '0.7em' },
186
+ h3: { spacingBefore: '1.9em', spacingAfter: '0.6em' },
187
+ h4: { spacingBefore: '1.35em', spacingAfter: '0.5em' },
188
+ },
189
+ body: {
190
+ paragraphSpacing: '1em',
191
+ },
192
+ caption: {
193
+ spacingBefore: '0.65em',
167
194
  },
168
195
  },
169
196
  };
170
197
 
171
198
  export const defaultTypography = {
172
199
  preset: 'quiet-gallery',
200
+ rhythm: 'normal',
173
201
  };
174
202
 
175
203
  const isPlainObject = (value) => (
@@ -196,26 +224,38 @@ export const mergeDeep = (base, override) => {
196
224
 
197
225
  export const resolveTypographyConfig = (typography = defaultTypography) => {
198
226
  const presetName = typography?.preset ?? defaultTypography.preset;
227
+ const rhythmName = typography?.rhythm ?? defaultTypography.rhythm;
199
228
  const preset = typographyPresets[presetName];
229
+ const rhythm = typographyRhythms[rhythmName];
200
230
 
201
231
  if (!preset) {
202
232
  throw new Error(`Unknown typography preset: ${presetName}`);
203
233
  }
204
234
 
235
+ if (!rhythm) {
236
+ throw new Error(`Unknown typography rhythm: ${rhythmName}`);
237
+ }
238
+
205
239
  return {
206
240
  preset: presetName,
207
- values: mergeDeep(preset, typography?.overrides),
241
+ rhythm: rhythmName,
242
+ values: mergeDeep(mergeDeep(preset, rhythm), typography?.overrides),
208
243
  };
209
244
  };
210
245
 
211
246
  export const resolveTypographyOverride = (baseResolved, typographyConfig) => {
212
- if (typographyConfig?.preset) {
213
- return resolveTypographyConfig(typographyConfig);
247
+ if (typographyConfig?.preset || typographyConfig?.rhythm) {
248
+ return resolveTypographyConfig({
249
+ preset: typographyConfig.preset ?? baseResolved.preset,
250
+ rhythm: typographyConfig.rhythm ?? baseResolved.rhythm,
251
+ overrides: typographyConfig.overrides,
252
+ });
214
253
  }
215
254
 
216
255
  if (typographyConfig?.overrides) {
217
256
  return {
218
257
  preset: baseResolved.preset,
258
+ rhythm: baseResolved.rhythm,
219
259
  values: mergeDeep(baseResolved.values, typographyConfig.overrides),
220
260
  };
221
261
  }
@@ -6,6 +6,7 @@ import {
6
6
  resolveTypographyConfig,
7
7
  toYamlLines,
8
8
  typographyPresets,
9
+ typographyRhythms,
9
10
  } from './lib/typography.mjs';
10
11
  import {
11
12
  getContentFiles,
@@ -29,22 +30,26 @@ const typographyValuePaths = [
29
30
  ['headings', 'h1', 'align', 'mobile'],
30
31
  ['headings', 'h1', 'size'],
31
32
  ['headings', 'h1', 'lineHeight'],
32
- ['headings', 'h1', 'spacing'],
33
+ ['headings', 'h1', 'spacingBefore'],
34
+ ['headings', 'h1', 'spacingAfter'],
33
35
  ['headings', 'h2', 'align', 'desktop'],
34
36
  ['headings', 'h2', 'align', 'mobile'],
35
37
  ['headings', 'h2', 'size'],
36
38
  ['headings', 'h2', 'lineHeight'],
37
- ['headings', 'h2', 'spacing'],
39
+ ['headings', 'h2', 'spacingBefore'],
40
+ ['headings', 'h2', 'spacingAfter'],
38
41
  ['headings', 'h3', 'align', 'desktop'],
39
42
  ['headings', 'h3', 'align', 'mobile'],
40
43
  ['headings', 'h3', 'size'],
41
44
  ['headings', 'h3', 'lineHeight'],
42
- ['headings', 'h3', 'spacing'],
45
+ ['headings', 'h3', 'spacingBefore'],
46
+ ['headings', 'h3', 'spacingAfter'],
43
47
  ['headings', 'h4', 'align', 'desktop'],
44
48
  ['headings', 'h4', 'align', 'mobile'],
45
49
  ['headings', 'h4', 'size'],
46
50
  ['headings', 'h4', 'lineHeight'],
47
- ['headings', 'h4', 'spacing'],
51
+ ['headings', 'h4', 'spacingBefore'],
52
+ ['headings', 'h4', 'spacingAfter'],
48
53
  ['body', 'align', 'desktop'],
49
54
  ['body', 'align', 'mobile'],
50
55
  ['body', 'size'],
@@ -54,8 +59,20 @@ const typographyValuePaths = [
54
59
  ['caption', 'align', 'mobile'],
55
60
  ['caption', 'size'],
56
61
  ['caption', 'lineHeight'],
57
- ['caption', 'spacing'],
62
+ ['caption', 'spacingBefore'],
58
63
  ];
64
+ const rhythmValuePaths = new Set([
65
+ 'headings.h1.spacingBefore',
66
+ 'headings.h1.spacingAfter',
67
+ 'headings.h2.spacingBefore',
68
+ 'headings.h2.spacingAfter',
69
+ 'headings.h3.spacingBefore',
70
+ 'headings.h3.spacingAfter',
71
+ 'headings.h4.spacingBefore',
72
+ 'headings.h4.spacingAfter',
73
+ 'body.paragraphSpacing',
74
+ 'caption.spacingBefore',
75
+ ]);
59
76
  const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
60
77
 
61
78
  const isPlainObject = (value) => (
@@ -106,12 +123,15 @@ const annotateResolvedValues = (resolved, sources) => {
106
123
  return annotated;
107
124
  };
108
125
 
109
- const presetSources = (presetName) => {
126
+ const defaultSources = (presetName, rhythmName) => {
110
127
  const sources = {};
111
128
 
112
129
  for (const path of typographyValuePaths) {
130
+ const pathKey = path.join('.');
113
131
  setPath(sources, path, {
114
- source: `preset:${presetName}`,
132
+ source: rhythmValuePaths.has(pathKey)
133
+ ? `rhythm:${rhythmName}`
134
+ : `preset:${presetName}`,
115
135
  inherited: false,
116
136
  });
117
137
  }
@@ -151,7 +171,7 @@ const inheritedSources = (sources) => {
151
171
  const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
152
172
  const resolved = resolveTypographyConfig(typographyConfig ?? defaultTypography);
153
173
  const sources = applyOverrideSources(
154
- presetSources(resolved.preset),
174
+ defaultSources(resolved.preset, resolved.rhythm),
155
175
  typographyConfig,
156
176
  sourceLabel,
157
177
  );
@@ -162,14 +182,44 @@ const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
162
182
  source: typographyConfig?.preset ? sourceLabel : 'engine default',
163
183
  ...(typographyConfig?.preset ? {} : { inherited: true }),
164
184
  },
185
+ rhythm: {
186
+ value: resolved.rhythm,
187
+ source: typographyConfig?.rhythm ? sourceLabel : 'engine default',
188
+ ...(typographyConfig?.rhythm ? {} : { inherited: true }),
189
+ },
165
190
  resolved,
166
191
  sources,
167
192
  };
168
193
  };
169
194
 
170
195
  const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sourceLabel) => {
171
- if (typographyConfig?.preset) {
172
- return resolveAnnotatedTypographyConfig(typographyConfig, sourceLabel);
196
+ if (typographyConfig?.preset || typographyConfig?.rhythm) {
197
+ const resolvedConfig = {
198
+ preset: typographyConfig.preset ?? baseAnnotated.resolved.preset,
199
+ rhythm: typographyConfig.rhythm ?? baseAnnotated.resolved.rhythm,
200
+ overrides: typographyConfig.overrides,
201
+ };
202
+ const resolved = resolveTypographyConfig(resolvedConfig);
203
+ const sources = applyOverrideSources(
204
+ defaultSources(resolved.preset, resolved.rhythm),
205
+ typographyConfig,
206
+ sourceLabel,
207
+ );
208
+
209
+ return {
210
+ preset: {
211
+ value: resolved.preset,
212
+ source: typographyConfig.preset ? sourceLabel : baseAnnotated.preset.source,
213
+ ...(typographyConfig.preset ? {} : { inherited: true }),
214
+ },
215
+ rhythm: {
216
+ value: resolved.rhythm,
217
+ source: typographyConfig.rhythm ? sourceLabel : baseAnnotated.rhythm.source,
218
+ ...(typographyConfig.rhythm ? {} : { inherited: true }),
219
+ },
220
+ resolved,
221
+ sources,
222
+ };
173
223
  }
174
224
 
175
225
  if (typographyConfig?.overrides) {
@@ -186,6 +236,11 @@ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sou
186
236
  source: baseAnnotated.preset.source,
187
237
  inherited: true,
188
238
  },
239
+ rhythm: {
240
+ value: resolved.rhythm,
241
+ source: baseAnnotated.rhythm.source,
242
+ inherited: true,
243
+ },
189
244
  resolved,
190
245
  sources,
191
246
  };
@@ -197,6 +252,11 @@ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sou
197
252
  source: baseAnnotated.preset.source,
198
253
  inherited: true,
199
254
  },
255
+ rhythm: {
256
+ value: baseAnnotated.resolved.rhythm,
257
+ source: baseAnnotated.rhythm.source,
258
+ inherited: true,
259
+ },
200
260
  resolved: baseAnnotated.resolved,
201
261
  sources: inheritedSources(baseAnnotated.sources),
202
262
  };
@@ -204,6 +264,7 @@ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sou
204
264
 
205
265
  const formatAnnotatedTypography = (annotated) => ({
206
266
  preset: annotated.preset,
267
+ rhythm: annotated.rhythm,
207
268
  resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
208
269
  });
209
270
 
@@ -305,7 +366,10 @@ const readPageTypography = async (contentFile, themeTypography) => {
305
366
  };
306
367
 
307
368
  if (mode === 'presets') {
308
- console.log(toYamlLines(typographyPresets).join('\n'));
369
+ console.log(toYamlLines({
370
+ presets: typographyPresets,
371
+ rhythms: typographyRhythms,
372
+ }).join('\n'));
309
373
  } else if (mode === 'show') {
310
374
  const themeTypography = await readThemeTypography();
311
375
  const pages = await Promise.all((await getContentFiles()).map((contentFile) => readPageTypography(contentFile, themeTypography)));
@@ -4,6 +4,7 @@ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promi
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { typographyPresets, typographyRhythms } from './lib/typography.mjs';
7
8
 
8
9
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
10
  const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
@@ -51,6 +52,23 @@ try {
51
52
  assert.equal(presetsResult.status, 0, presetsResult.stderr || presetsResult.stdout);
52
53
  assert.match(presetsResult.stdout, /quiet-gallery:/);
53
54
  assert.match(presetsResult.stdout, /text-forward:/);
55
+ assert.match(presetsResult.stdout, /rhythms:/);
56
+ assert.match(presetsResult.stdout, /normal:/);
57
+ for (const [presetName, preset] of Object.entries(typographyPresets)) {
58
+ for (const [level, heading] of Object.entries(preset.headings)) {
59
+ assert.equal(heading.size, 'medium', `${presetName} ${level} should use medium size`);
60
+ }
61
+ assert.equal(preset.body.size, 'medium', `${presetName} body should use medium size`);
62
+ assert.equal(preset.caption.size, 'medium', `${presetName} caption should use medium size`);
63
+ }
64
+ for (const [rhythmName, rhythm] of Object.entries(typographyRhythms)) {
65
+ for (const [level, heading] of Object.entries(rhythm.headings)) {
66
+ assert.match(heading.spacingBefore, /^(0|[\d.]+em)$/, `${rhythmName} ${level} spacingBefore should be text-relative`);
67
+ assert.match(heading.spacingAfter, /^[\d.]+em$/, `${rhythmName} ${level} spacingAfter should be text-relative`);
68
+ }
69
+ assert.match(rhythm.body.paragraphSpacing, /^[\d.]+em$/, `${rhythmName} body paragraphSpacing should be text-relative`);
70
+ assert.match(rhythm.caption.spacingBefore, /^[\d.]+em$/, `${rhythmName} caption spacingBefore should be text-relative`);
71
+ }
54
72
 
55
73
  const showResult = runCli(['--site-dir', path.join(initializedSiteRoot, 'site'), 'typography', 'show']);
56
74
  assert.equal(showResult.status, 0, showResult.stderr || showResult.stdout);
@@ -235,7 +235,15 @@ This route verifies that packaged norna sites can build route pages.
235
235
  );
236
236
  await assertFileIncludes(
237
237
  path.join(siteProjectRoot, 'dist', 'index.html'),
238
- '--section-heading-font-size-desktop: clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem)',
238
+ '--section-heading-font-size-desktop: clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
239
+ );
240
+ await assertFileIncludes(
241
+ path.join(siteProjectRoot, 'dist', 'index.html'),
242
+ '--section-heading-spacing-after: 0.55em',
243
+ );
244
+ await assertFileIncludes(
245
+ path.join(siteProjectRoot, 'dist', 'index.html'),
246
+ '--section-markdown-h3-spacing-before: 1.5em',
239
247
  );
240
248
  await assertFileIncludes(
241
249
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -273,6 +281,14 @@ This route verifies that packaged norna sites can build route pages.
273
281
  path.join(siteProjectRoot, 'dist', 'index.html'),
274
282
  '--section-caption-line-height: 1.35',
275
283
  );
284
+ await assertFileIncludes(
285
+ path.join(siteProjectRoot, 'dist', 'index.html'),
286
+ '--section-caption-spacing-before: 0.5em',
287
+ );
288
+ await assertFileIncludes(
289
+ path.join(siteProjectRoot, 'dist', 'index.html'),
290
+ '--section-caption-font-size-desktop: clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
291
+ );
276
292
  await assertFileIncludes(
277
293
  path.join(siteProjectRoot, 'dist', 'index.html'),
278
294
  'class="inline-style inline-style-highlight" style="--inline-style-color: #ffd84d"',
@@ -371,14 +387,14 @@ This route verifies that packaged norna sites can build route pages.
371
387
  siteContentPath,
372
388
  siteContent.replace(
373
389
  ' typography:\n preset: statement',
374
- ' typography:\n overrides:\n caption:\n spacing: wide',
390
+ ' typography:\n overrides:\n caption:\n spacingBefore: wide',
375
391
  ),
376
392
  );
377
393
  await runExpectFailure(
378
394
  npxBin,
379
395
  ['norna', 'build'],
380
396
  { cwd: siteProjectRoot, env: npmEnv },
381
- 'sections.0.presentation.typography.overrides.caption.spacing',
397
+ 'sections.0.presentation.typography.overrides.caption.spacingBefore',
382
398
  );
383
399
  await writeFile(
384
400
  siteContentPath,