@janga/norna 0.7.2 → 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 (88) hide show
  1. package/README.md +16 -16
  2. package/bin/norna-cli.mjs +12 -7
  3. package/docs/README.md +11 -12
  4. package/docs/commands.md +9 -9
  5. package/docs/configuration.md +14 -145
  6. package/docs/content.md +17 -15
  7. package/docs/design/command-organization.md +58 -57
  8. package/docs/design/site-examples-structure.md +6 -5
  9. package/docs/engine-development.md +5 -5
  10. package/docs/getting-started.md +20 -26
  11. package/docs/images-and-metadata.md +1 -1
  12. package/docs/local-development.md +2 -2
  13. package/docs/publishing.md +1 -1
  14. package/docs/routes.md +2 -2
  15. package/docs/site-structure.md +8 -7
  16. package/docs/theme.md +133 -7
  17. package/docs/typography.md +78 -43
  18. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +74 -0
  19. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  20. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  21. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  22. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  23. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  24. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  25. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  26. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  27. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  28. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  29. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  30. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  31. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  32. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  33. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  34. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  35. package/examples/dog-gallery/node_modules/.vite/deps/package.json +3 -0
  36. package/examples/dog-gallery/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  37. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  38. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  39. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  40. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  41. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  42. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  43. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  44. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  45. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  46. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  47. package/examples/dog-gallery/site/config.mjs +12 -42
  48. package/examples/dog-gallery/site/content.md +28 -6
  49. package/examples/dog-gallery/site/routes/dog-care/route-content.md +17 -3
  50. package/examples/dog-gallery/site/theme.md +83 -26
  51. package/fixtures/basic/site/config.mjs +2 -22
  52. package/fixtures/basic/site/content.md +1 -1
  53. package/fixtures/basic/site/theme.md +7 -3
  54. package/package.json +3 -3
  55. package/scripts/check-config.mjs +4 -3
  56. package/scripts/deploy-site.mjs +3 -3
  57. package/scripts/init-site.mjs +10 -8
  58. package/scripts/lib/frontmatter-yaml.mjs +85 -0
  59. package/scripts/lib/presentation.mjs +10 -11
  60. package/scripts/lib/project-config.mjs +146 -45
  61. package/scripts/lib/site-content.mjs +21 -3
  62. package/scripts/lib/theme-config.mjs +34 -0
  63. package/scripts/lib/typography.mjs +155 -35
  64. package/scripts/show-typography.mjs +127 -159
  65. package/scripts/sync-content-sections.mjs +6 -6
  66. package/scripts/test-content-check.mjs +3 -4
  67. package/scripts/test-engine-commands.mjs +21 -3
  68. package/scripts/test-package-check.mjs +34 -17
  69. package/scripts/test-temporary-visibility.mjs +2 -3
  70. package/src/components/SitePage.astro +2 -3
  71. package/src/components/SiteSection.astro +115 -19
  72. package/src/content.config.ts +66 -6
  73. package/src/layouts/BaseLayout.astro +11 -0
  74. package/src/styles/global.css +83 -52
  75. package/starters/basic/README.md +11 -15
  76. package/starters/basic/package.json +2 -2
  77. package/starters/basic/site/config.mjs +3 -24
  78. package/starters/basic/site/content.md +40 -7
  79. package/starters/basic/site/theme.md +84 -18
  80. package/starters/project/.github/workflows/deploy.yml +65 -0
  81. package/starters/project/README.md +72 -0
  82. package/starters/project/package-lock.json +4296 -0
  83. package/starters/project/package.json +35 -0
  84. package/starters/project/site/config.mjs +43 -0
  85. package/starters/project/site/content.md +128 -0
  86. package/starters/project/site/public/robots.txt +2 -0
  87. package/starters/project/site/routes/guide/route-content.md +85 -0
  88. package/starters/project/site/theme.md +43 -0
@@ -32,22 +32,81 @@ interface Props {
32
32
 
33
33
  const { section, pagePresentation, headingLevel, priorityGalleryImage = false } = Astro.props;
34
34
  const HeadingTag = headingLevel === 1 ? 'h1' : 'h2';
35
- const headingSizeValues: Record<TextSize, ResponsiveValue<string>> = {
36
- small: {
37
- desktop: 'clamp(1.1rem, 1.02rem + 0.35vw, 1.35rem)',
38
- mobile: 'clamp(1.12rem, 1rem + 0.85vw, 1.42rem)',
35
+ // Sections are authored with Markdown ## headings even when the first one renders as HTML h1.
36
+ const sectionHeadingVisualLevel = 'h2';
37
+ type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4';
38
+ const headingSizeValues: Record<HeadingLevel, Record<TextSize, ResponsiveValue<string>>> = {
39
+ h1: {
40
+ small: {
41
+ desktop: 'clamp(1.5rem, 1.18rem + 1.15vw, 2.25rem)',
42
+ mobile: 'clamp(1.42rem, 1.12rem + 1.5vw, 2rem)',
43
+ },
44
+ medium: {
45
+ desktop: 'clamp(1.85rem, 1.35rem + 1.8vw, 3.1rem)',
46
+ mobile: 'clamp(1.65rem, 1.18rem + 2.2vw, 2.45rem)',
47
+ },
48
+ large: {
49
+ desktop: 'clamp(2.25rem, 1.45rem + 2.75vw, 4.25rem)',
50
+ mobile: 'clamp(1.9rem, 1.26rem + 3.2vw, 3.15rem)',
51
+ },
52
+ xlarge: {
53
+ desktop: 'clamp(2.75rem, 1.6rem + 3.8vw, 5.5rem)',
54
+ mobile: 'clamp(2.2rem, 1.35rem + 4.4vw, 3.9rem)',
55
+ },
39
56
  },
40
- medium: {
41
- desktop: 'clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
42
- mobile: 'clamp(1.3rem, 1.05rem + 1.35vw, 1.75rem)',
57
+ h2: {
58
+ small: {
59
+ desktop: 'clamp(1.1rem, 1.02rem + 0.35vw, 1.35rem)',
60
+ mobile: 'clamp(1.12rem, 1rem + 0.85vw, 1.42rem)',
61
+ },
62
+ medium: {
63
+ desktop: 'clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
64
+ mobile: 'clamp(1.3rem, 1.05rem + 1.35vw, 1.75rem)',
65
+ },
66
+ large: {
67
+ desktop: 'clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem)',
68
+ mobile: 'clamp(1.5rem, 1.12rem + 2vw, 2.25rem)',
69
+ },
70
+ xlarge: {
71
+ desktop: 'clamp(2rem, 1.35rem + 2.4vw, 3.75rem)',
72
+ mobile: 'clamp(1.8rem, 1.25rem + 3vw, 2.85rem)',
73
+ },
43
74
  },
44
- large: {
45
- desktop: 'clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem)',
46
- mobile: 'clamp(1.5rem, 1.12rem + 2vw, 2.25rem)',
75
+ h3: {
76
+ small: {
77
+ desktop: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
78
+ mobile: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
79
+ },
80
+ medium: {
81
+ desktop: 'clamp(1.05rem, 0.98rem + 0.28vw, 1.22rem)',
82
+ mobile: 'clamp(1.04rem, 0.98rem + 0.45vw, 1.18rem)',
83
+ },
84
+ large: {
85
+ desktop: 'clamp(1.16rem, 1.02rem + 0.5vw, 1.5rem)',
86
+ mobile: 'clamp(1.12rem, 1rem + 0.75vw, 1.36rem)',
87
+ },
88
+ xlarge: {
89
+ desktop: 'clamp(1.32rem, 1.08rem + 0.85vw, 1.9rem)',
90
+ mobile: 'clamp(1.24rem, 1.04rem + 1.15vw, 1.65rem)',
91
+ },
47
92
  },
48
- xlarge: {
49
- desktop: 'clamp(2rem, 1.35rem + 2.4vw, 3.75rem)',
50
- mobile: 'clamp(1.8rem, 1.25rem + 3vw, 2.85rem)',
93
+ h4: {
94
+ small: {
95
+ desktop: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
96
+ mobile: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
97
+ },
98
+ medium: {
99
+ desktop: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
100
+ mobile: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
101
+ },
102
+ large: {
103
+ desktop: 'clamp(1.05rem, 1rem + 0.16vw, 1.12rem)',
104
+ mobile: 'clamp(1.05rem, 1rem + 0.16vw, 1.12rem)',
105
+ },
106
+ xlarge: {
107
+ desktop: 'clamp(1.14rem, 1.06rem + 0.26vw, 1.25rem)',
108
+ mobile: 'clamp(1.14rem, 1.06rem + 0.26vw, 1.25rem)',
109
+ },
51
110
  },
52
111
  };
53
112
  const bodySizeValues: Record<TextSize, ResponsiveValue<string>> = {
@@ -91,16 +150,37 @@ const getSizeValue = (
91
150
  sizeValues: Record<TextSize, ResponsiveValue<string>>,
92
151
  size: TextSize,
93
152
  ) => sizeValues[size];
153
+ const getHeadingSizeValue = (level: HeadingLevel, size: TextSize) => (
154
+ headingSizeValues[level][size]
155
+ );
94
156
  const backgroundColor = sectionOverride?.backgroundColor
95
157
  ?? pagePresentation.backgroundColor;
96
158
  const textColor = sectionOverride?.textColor
97
159
  ?? pagePresentation.textColor;
98
160
  const resolvedTypography = resolveTypographyOverride(pagePresentation.typography, sectionOverride?.typography);
99
161
  const typography = resolvedTypography.values;
100
- const headingAlign = typography.heading.align as ResponsiveValue<TextAlign>;
162
+ const heading = typography.headings[sectionHeadingVisualLevel] as {
163
+ align: ResponsiveValue<TextAlign>;
164
+ size: TextSize;
165
+ lineHeight: number;
166
+ spacingBefore: string;
167
+ spacingAfter: string;
168
+ };
169
+ const markdownHeadingLevels = ['h3', 'h4'] as const;
170
+ const markdownHeadings = Object.fromEntries(markdownHeadingLevels.map((level) => [
171
+ level,
172
+ typography.headings[level] as {
173
+ align: ResponsiveValue<TextAlign>;
174
+ size: TextSize;
175
+ lineHeight: number;
176
+ spacingBefore: string;
177
+ spacingAfter: string;
178
+ },
179
+ ]));
180
+ const headingAlign = heading.align;
101
181
  const bodyAlign = typography.body.align as ResponsiveValue<TextAlign>;
102
182
  const captionAlign = typography.caption.align as ResponsiveValue<TextAlign>;
103
- const headingFontSize = getSizeValue(headingSizeValues, typography.heading.size as TextSize);
183
+ const headingFontSize = getHeadingSizeValue(sectionHeadingVisualLevel, heading.size);
104
184
  const bodyFontSize = getSizeValue(bodySizeValues, typography.body.size as TextSize);
105
185
  const captionFontSize = getSizeValue(captionSizeValues, typography.caption.size as TextSize);
106
186
  const justifyByAlign: Record<TextAlign, string> = {
@@ -118,6 +198,20 @@ const getAlignedTextWidth = (
118
198
  const headingWidth = getAlignedTextWidth(headingAlign, '760px');
119
199
  const bodyWidth = getAlignedTextWidth(bodyAlign, 'var(--text-width)');
120
200
  const hasGallery = section.gallery.length > 0;
201
+ const markdownHeadingStyle = (level: 'h3' | 'h4') => {
202
+ const headingConfig = markdownHeadings[level];
203
+ const fontSize = getHeadingSizeValue(level, headingConfig.size);
204
+
205
+ return [
206
+ `--section-markdown-${level}-align-desktop: ${headingConfig.align.desktop}`,
207
+ `--section-markdown-${level}-align-mobile: ${headingConfig.align.mobile}`,
208
+ `--section-markdown-${level}-font-size-desktop: ${fontSize.desktop}`,
209
+ `--section-markdown-${level}-font-size-mobile: ${fontSize.mobile}`,
210
+ `--section-markdown-${level}-line-height: ${headingConfig.lineHeight}`,
211
+ `--section-markdown-${level}-spacing-before: ${headingConfig.spacingBefore}`,
212
+ `--section-markdown-${level}-spacing-after: ${headingConfig.spacingAfter}`,
213
+ ];
214
+ };
121
215
  const sectionStyle = [
122
216
  `--section-background-color: ${backgroundColor}`,
123
217
  `--section-text-color: ${textColor}`,
@@ -125,10 +219,12 @@ const sectionStyle = [
125
219
  `--section-heading-align-mobile: ${headingAlign.mobile}`,
126
220
  `--section-heading-font-size-desktop: ${headingFontSize.desktop}`,
127
221
  `--section-heading-font-size-mobile: ${headingFontSize.mobile}`,
128
- `--section-heading-line-height: ${typography.heading.lineHeight}`,
129
- `--section-heading-spacing: ${typography.heading.spacing}`,
222
+ `--section-heading-line-height: ${heading.lineHeight}`,
223
+ `--section-heading-spacing-after: ${heading.spacingAfter}`,
130
224
  `--section-heading-width-desktop: ${headingWidth.desktop}`,
131
225
  `--section-heading-width-mobile: ${headingWidth.mobile}`,
226
+ ...markdownHeadingStyle('h3'),
227
+ ...markdownHeadingStyle('h4'),
132
228
  `--section-body-align-desktop: ${bodyAlign.desktop}`,
133
229
  `--section-body-align-mobile: ${bodyAlign.mobile}`,
134
230
  `--section-body-font-size-desktop: ${bodyFontSize.desktop}`,
@@ -142,7 +238,7 @@ const sectionStyle = [
142
238
  `--section-caption-font-size-desktop: ${captionFontSize.desktop}`,
143
239
  `--section-caption-font-size-mobile: ${captionFontSize.mobile}`,
144
240
  `--section-caption-line-height: ${typography.caption.lineHeight}`,
145
- `--section-caption-spacing: ${typography.caption.spacing}`,
241
+ `--section-caption-spacing-before: ${typography.caption.spacingBefore}`,
146
242
  `--section-caption-justify-desktop: ${justifyByAlign[captionAlign.desktop]}`,
147
243
  `--section-caption-justify-mobile: ${justifyByAlign[captionAlign.mobile]}`,
148
244
  ].join('; ');
@@ -150,7 +246,7 @@ const sectionStyle = [
150
246
 
151
247
  <section
152
248
  id={section.id}
153
- class:list={['site-section', { 'site-section-primary': headingLevel === 1 }]}
249
+ class="site-section"
154
250
  data-section-title={section.title}
155
251
  style={sectionStyle}
156
252
  >
@@ -18,6 +18,7 @@ const colorValue = z.string().regex(
18
18
  const textAlign = z.enum(['left', 'center', 'right']);
19
19
  const textSize = z.enum(['small', 'medium', 'large', 'xlarge']);
20
20
  const typographyPreset = z.enum(['quiet-gallery', 'compact-gallery', 'text-forward', 'statement']);
21
+ const spacingDensity = z.enum(['compact', 'normal', 'airy']);
21
22
  const lineHeight = z.number()
22
23
  .min(1, 'Use a unitless line height of at least 1.')
23
24
  .max(3, 'Use a unitless line height of at most 3.');
@@ -25,6 +26,12 @@ const cssLength = z.string().regex(
25
26
  /^(?:0|(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|lh))$/,
26
27
  'Use a CSS length such as "0", "0.8em", "1rem", or "12px".',
27
28
  );
29
+ const visualCssLengthUnit = String.raw`(?:px|rem|em|vw|vh|vmin|vmax|ch|%)`;
30
+ const visualCssLengthValue = String.raw`(?:\d+(?:\.\d+)?|\.\d+)${visualCssLengthUnit}`;
31
+ const visualCssLength = z.string().regex(
32
+ new RegExp(`^(?:0|${visualCssLengthValue}|clamp\\(\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*\\))$`),
33
+ 'Use a CSS length such as "0", "900px", "56rem", "90%", or "clamp(1rem, 4vw, 3rem)".',
34
+ );
28
35
  const inlineStyleName = z.string().regex(/^[a-z][a-z0-9-]*$/);
29
36
  const dateOnly = z.string()
30
37
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format.')
@@ -53,7 +60,14 @@ const commonTextPresentationOverride = {
53
60
  };
54
61
  const headingPresentationOverride = z.object({
55
62
  ...commonTextPresentationOverride,
56
- spacing: cssLength.optional(),
63
+ spacingBefore: cssLength.optional(),
64
+ spacingAfter: cssLength.optional(),
65
+ }).strict();
66
+ const headingLevelsPresentationOverride = z.object({
67
+ h1: headingPresentationOverride.optional(),
68
+ h2: headingPresentationOverride.optional(),
69
+ h3: headingPresentationOverride.optional(),
70
+ h4: headingPresentationOverride.optional(),
57
71
  }).strict();
58
72
  const bodyPresentationOverride = z.object({
59
73
  ...commonTextPresentationOverride,
@@ -61,20 +75,64 @@ const bodyPresentationOverride = z.object({
61
75
  }).strict();
62
76
  const captionPresentationOverride = z.object({
63
77
  ...commonTextPresentationOverride,
64
- spacing: cssLength.optional(),
78
+ spacingBefore: cssLength.optional(),
65
79
  }).strict();
66
80
  const typographyOverrides = z.object({
67
- heading: headingPresentationOverride.optional(),
81
+ headings: headingLevelsPresentationOverride.optional(),
68
82
  body: bodyPresentationOverride.optional(),
69
83
  caption: captionPresentationOverride.optional(),
70
84
  }).strict();
71
85
  const typography = z.object({
72
86
  preset: typographyPreset.optional(),
87
+ rhythm: spacingDensity.optional(),
88
+ overrides: typographyOverrides.optional(),
89
+ }).strict().refine(
90
+ (value) => value.preset !== undefined || value.rhythm !== undefined || value.overrides !== undefined,
91
+ 'Specify preset, rhythm, overrides, or both.',
92
+ );
93
+ const themeTypography = z.object({
94
+ fontFamily: z.string()
95
+ .min(1)
96
+ .refine((value) => !/[\n\r;{}]/.test(value), 'Do not use semicolons, braces, or line breaks.').optional(),
97
+ preset: typographyPreset.optional(),
98
+ rhythm: spacingDensity.optional(),
73
99
  overrides: typographyOverrides.optional(),
74
100
  }).strict().refine(
75
- (value) => value.preset !== undefined || value.overrides !== undefined,
76
- 'Specify preset, overrides, or both.',
101
+ (value) => value.fontFamily !== undefined || value.preset !== undefined || value.rhythm !== undefined || value.overrides !== undefined,
102
+ 'Specify fontFamily, preset, rhythm, overrides, or both.',
77
103
  );
104
+ const responsiveCssLength = z.union([
105
+ visualCssLength,
106
+ z.object({
107
+ desktop: visualCssLength,
108
+ mobile: visualCssLength,
109
+ }).strict(),
110
+ ]);
111
+ const responsivePercent = z.union([
112
+ z.number().positive().max(100),
113
+ z.object({
114
+ desktop: z.number().positive().max(100),
115
+ mobile: z.number().positive().max(100),
116
+ }).strict(),
117
+ ]);
118
+ const themeLayoutSpacing = z.object({
119
+ bodyToImages: responsiveCssLength.optional(),
120
+ finalSectionBottom: responsiveCssLength.optional(),
121
+ firstSectionTop: responsiveCssLength.optional(),
122
+ imageGap: responsiveCssLength.optional(),
123
+ sectionGap: responsiveCssLength.optional(),
124
+ }).strict();
125
+ const themeLayout = z.object({
126
+ density: spacingDensity.optional(),
127
+ pageWidth: visualCssLength.optional(),
128
+ gutter: responsiveCssLength.optional(),
129
+ spacing: themeLayoutSpacing.optional(),
130
+ }).strict();
131
+ const themeGallery = z.object({
132
+ width: visualCssLength.optional(),
133
+ maxAvailableWidthPercent: responsivePercent.optional(),
134
+ maxAvailableHeightPercent: responsivePercent.optional(),
135
+ }).strict();
78
136
  const sectionPresentationOverride = z.object({
79
137
  backgroundColor: colorValue.optional(),
80
138
  textColor: colorValue.optional(),
@@ -86,7 +144,6 @@ const themePresentation = z.object({
86
144
  inlineStyles: z.record(inlineStyleName, z.object({
87
145
  color: colorValue,
88
146
  }).strict()).optional(),
89
- typography: typography.optional(),
90
147
  }).strict();
91
148
  const pagePresentation = z.object({
92
149
  backgroundColor: colorValue.optional(),
@@ -141,6 +198,9 @@ const siteSchema = z.object({
141
198
 
142
199
  const themeSchema = z.object({
143
200
  navigation: themeNavigation.optional(),
201
+ layout: themeLayout.optional(),
202
+ gallery: themeGallery.optional(),
203
+ typography: themeTypography.optional(),
144
204
  presentation: themePresentation.optional(),
145
205
  frame: frame.optional(),
146
206
  }).strict();
@@ -29,12 +29,23 @@ const iconLinks = getIconLinks();
29
29
  const scrollBehavior = projectConfig.navigation.smoothScroll.enabled ? 'smooth' : 'auto';
30
30
  const formatPercent = (value: number) => `${value}%`;
31
31
  const formatViewportHeightPercent = (value: number) => `${value}svh`;
32
+ const layoutSpacing = projectConfig.layout.spacing;
32
33
  const documentStyle = [
33
34
  `scroll-behavior: ${scrollBehavior}`,
34
35
  `--font-sans: ${projectConfig.typography.fontFamily}`,
35
36
  `--page-width: ${projectConfig.layout.pageWidth}`,
36
37
  `--content-gutter-desktop: ${projectConfig.layout.gutter.desktop}`,
37
38
  `--content-gutter-mobile: ${projectConfig.layout.gutter.mobile}`,
39
+ `--space-nav-to-first-section-desktop: ${layoutSpacing.firstSectionTop.desktop}`,
40
+ `--space-nav-to-first-section-mobile: ${layoutSpacing.firstSectionTop.mobile}`,
41
+ `--space-section-body-to-gallery-desktop: ${layoutSpacing.bodyToImages.desktop}`,
42
+ `--space-section-body-to-gallery-mobile: ${layoutSpacing.bodyToImages.mobile}`,
43
+ `--space-gallery-item-desktop: ${layoutSpacing.imageGap.desktop}`,
44
+ `--space-gallery-item-mobile: ${layoutSpacing.imageGap.mobile}`,
45
+ `--space-section-to-section-desktop: ${layoutSpacing.sectionGap.desktop}`,
46
+ `--space-section-to-section-mobile: ${layoutSpacing.sectionGap.mobile}`,
47
+ `--space-final-section-bottom-desktop: ${layoutSpacing.finalSectionBottom.desktop}`,
48
+ `--space-final-section-bottom-mobile: ${layoutSpacing.finalSectionBottom.mobile}`,
38
49
  `--gallery-width: ${projectConfig.gallery.width}`,
39
50
  `--gallery-max-available-width-desktop: ${formatPercent(projectConfig.gallery.maxAvailableWidthPercent.desktop)}`,
40
51
  `--gallery-max-available-width-mobile: ${formatPercent(projectConfig.gallery.maxAvailableWidthPercent.mobile)}`,
@@ -28,20 +28,21 @@
28
28
  --gallery-max-image-height: var(--gallery-max-image-height-desktop);
29
29
  --nav-top-safety-inset: 0px;
30
30
 
31
- /* Avstand fran sticky navigationsrad till forsta sektionsrubriken. */
32
- --space-nav-to-first-section: clamp(1.875rem, 3vw, 2.75rem);
33
-
34
- /* Avstand fran sektionsrubrik till sektionens forsta textstycke. */
35
- --space-section-title-to-body: clamp(0.5625rem, 1.25vw, 0.75rem);
36
-
37
- /* Avstand fran sektionens lopande text till efterfoljande galleri. */
38
- --space-section-body-to-gallery: clamp(1.25rem, 2.5vw, 2rem);
39
-
40
- /* Avstand mellan enskilda bilder/verk inom samma galleri. */
41
- --space-gallery-item: clamp(1.5rem, 3.5vw, 2.75rem);
42
-
43
- /* Avstand fran sista galleriobjektet eller textblocket till nasta sektion. */
44
- --space-section-to-section: clamp(2.55rem, 4.2vw, 3.9rem);
31
+ --space-nav-to-first-section-desktop: clamp(1.875rem, 3vw, 2.75rem);
32
+ --space-nav-to-first-section-mobile: 1.375rem;
33
+ --space-nav-to-first-section: var(--space-nav-to-first-section-desktop);
34
+ --space-section-body-to-gallery-desktop: clamp(1.25rem, 2.5vw, 2rem);
35
+ --space-section-body-to-gallery-mobile: 1.25rem;
36
+ --space-section-body-to-gallery: var(--space-section-body-to-gallery-desktop);
37
+ --space-gallery-item-desktop: clamp(1.5rem, 3.5vw, 2.75rem);
38
+ --space-gallery-item-mobile: 2.75rem;
39
+ --space-gallery-item: var(--space-gallery-item-desktop);
40
+ --space-section-to-section-desktop: clamp(2.55rem, 4.2vw, 3.9rem);
41
+ --space-section-to-section-mobile: 2.25rem;
42
+ --space-section-to-section: var(--space-section-to-section-desktop);
43
+ --space-final-section-bottom-desktop: var(--space-section-to-section-desktop);
44
+ --space-final-section-bottom-mobile: var(--space-section-to-section-mobile);
45
+ --space-final-section-bottom: var(--space-final-section-bottom-desktop);
45
46
 
46
47
  --content-gutter-desktop: clamp(1.25rem, 4vw, 3rem);
47
48
  --content-gutter-mobile: 1rem;
@@ -316,7 +317,7 @@ a {
316
317
  }
317
318
 
318
319
  .site-section:last-child {
319
- padding-bottom: var(--space-section-to-section);
320
+ padding-bottom: var(--space-final-section-bottom);
320
321
  }
321
322
 
322
323
  .section-card {
@@ -327,19 +328,19 @@ a {
327
328
  .section-header {
328
329
  width: min(100%, var(--section-heading-width-desktop, 760px));
329
330
  margin: 0 auto;
330
- padding: 0 0 var(--section-heading-spacing, var(--space-section-title-to-body));
331
+ padding: 0 0 var(--section-heading-spacing-after, 0.55em);
331
332
  text-align: var(--section-heading-align-desktop, center);
332
333
  }
333
334
 
334
- h1,
335
- h2,
335
+ .section-header h1,
336
+ .section-header h2,
336
337
  p,
337
338
  ul {
338
339
  margin-top: 0;
339
340
  }
340
341
 
341
- h1,
342
- h2 {
342
+ .section-header h1,
343
+ .section-header h2 {
343
344
  margin-bottom: 0;
344
345
  color: inherit;
345
346
  font-size: var(--section-heading-font-size-desktop, clamp(1.35rem, 1.1rem + 0.9vw, 2rem));
@@ -350,11 +351,6 @@ h2 {
350
351
  overflow-wrap: anywhere;
351
352
  }
352
353
 
353
- .site-section-primary h1 {
354
- font-size: var(--section-heading-font-size-desktop, clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem));
355
- font-weight: 700;
356
- }
357
-
358
354
  .section-body {
359
355
  width: 100%;
360
356
  margin: 0 auto;
@@ -375,6 +371,31 @@ h2 {
375
371
  margin: 0 0 var(--section-body-paragraph-spacing, 1em);
376
372
  }
377
373
 
374
+ .section-markdown blockquote {
375
+ margin: 0 0 var(--section-body-paragraph-spacing, 1em);
376
+ border-left: 1px solid color-mix(in srgb, currentColor 34%, transparent);
377
+ padding-left: 1rem;
378
+ }
379
+
380
+ .section-markdown blockquote > :last-child {
381
+ margin-bottom: 0;
382
+ }
383
+
384
+ .section-markdown pre.astro-code {
385
+ margin: 0 0 var(--section-body-paragraph-spacing, 1em);
386
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
387
+ border-radius: 6px;
388
+ padding: 0.78rem 0.9rem;
389
+ background: #0d1117 !important;
390
+ font-size: 0.92em;
391
+ line-height: 1.45;
392
+ text-align: left;
393
+ }
394
+
395
+ .section-markdown pre.astro-code code {
396
+ font: inherit;
397
+ }
398
+
378
399
  .section-markdown .inline-style {
379
400
  color: var(--inline-style-color);
380
401
  }
@@ -388,24 +409,40 @@ h2 {
388
409
  margin-bottom: 0;
389
410
  }
390
411
 
412
+ .section-markdown > pre.astro-code:last-child {
413
+ margin-bottom: 0;
414
+ }
415
+
416
+ .section-markdown blockquote > pre.astro-code:last-child {
417
+ margin-bottom: 0;
418
+ }
419
+
391
420
  .section-markdown h3 {
392
- margin: 2.75rem 0 0.9rem;
421
+ margin:
422
+ var(--section-markdown-h3-spacing-before, 1.5em)
423
+ 0
424
+ var(--section-markdown-h3-spacing-after, 0.5em);
393
425
  border-top: 1px solid var(--color-soft);
394
- padding-top: 1.75rem;
426
+ padding-top: 0.55em;
395
427
  font-family: var(--font-sans);
396
- font-size: 0.82rem;
397
- letter-spacing: 0.14em;
398
- line-height: 1.35;
428
+ font-size: var(--section-markdown-h3-font-size-desktop, 0.82rem);
429
+ letter-spacing: 0;
430
+ line-height: var(--section-markdown-h3-line-height, 1.35);
431
+ text-align: var(--section-markdown-h3-align-desktop, left);
399
432
  text-transform: uppercase;
400
433
  }
401
434
 
402
435
  .section-markdown h4 {
403
- margin: 0 0 0.85rem;
436
+ margin:
437
+ var(--section-markdown-h4-spacing-before, 1.1em)
438
+ 0
439
+ var(--section-markdown-h4-spacing-after, 0.4em);
404
440
  color: inherit;
405
- font-size: clamp(0.96rem, 0.94rem + 0.08vw, 1rem);
441
+ font-size: var(--section-markdown-h4-font-size-desktop, clamp(0.96rem, 0.94rem + 0.08vw, 1rem));
406
442
  font-weight: 400;
407
- letter-spacing: 0.02em;
408
- line-height: 1.5;
443
+ letter-spacing: 0;
444
+ line-height: var(--section-markdown-h4-line-height, 1.5);
445
+ text-align: var(--section-markdown-h4-align-desktop, left);
409
446
  }
410
447
 
411
448
  .section-markdown ul,
@@ -618,7 +655,7 @@ h2 {
618
655
 
619
656
  .gallery-meta {
620
657
  width: min(100%, var(--gallery-layout-width));
621
- margin: var(--section-caption-spacing, 0.5em) auto 0;
658
+ margin: var(--section-caption-spacing-before, 0.5em) auto 0;
622
659
  font-size: var(--section-caption-font-size-desktop, 0.95rem);
623
660
  text-align: var(--section-caption-align-desktop, center);
624
661
  }
@@ -639,11 +676,11 @@ h2 {
639
676
 
640
677
  @media (max-width: 700px) {
641
678
  :root {
642
- --space-nav-to-first-section: 1.375rem;
643
- --space-section-title-to-body: 0.4375rem;
644
- --space-section-body-to-gallery: 1.25rem;
645
- --space-gallery-item: 2.75rem;
646
- --space-section-to-section: 2.25rem;
679
+ --space-nav-to-first-section: var(--space-nav-to-first-section-mobile);
680
+ --space-section-body-to-gallery: var(--space-section-body-to-gallery-mobile);
681
+ --space-gallery-item: var(--space-gallery-item-mobile);
682
+ --space-section-to-section: var(--space-section-to-section-mobile);
683
+ --space-final-section-bottom: var(--space-final-section-bottom-mobile);
647
684
  --content-gutter: var(--content-gutter-mobile);
648
685
  --gallery-max-available-width: var(--gallery-max-available-width-mobile);
649
686
  --gallery-max-image-height: var(--gallery-max-image-height-mobile);
@@ -780,21 +817,14 @@ h2 {
780
817
  line-height: 1.72;
781
818
  }
782
819
 
783
- h1,
784
- h2 {
820
+ .section-header h1,
821
+ .section-header h2 {
785
822
  font-size: var(
786
823
  --section-heading-font-size-mobile,
787
824
  var(--section-heading-font-size-desktop, clamp(1.3rem, 1.05rem + 1.35vw, 1.75rem))
788
825
  );
789
826
  }
790
827
 
791
- .site-section-primary h1 {
792
- font-size: var(
793
- --section-heading-font-size-mobile,
794
- var(--section-heading-font-size-desktop, clamp(1.5rem, 1.12rem + 2vw, 2.25rem))
795
- );
796
- }
797
-
798
828
  .section-header {
799
829
  width: min(100%, var(--section-heading-width-mobile, var(--section-heading-width-desktop, 760px)));
800
830
  text-align: var(--section-heading-align-mobile, var(--section-heading-align-desktop, center));
@@ -819,12 +849,13 @@ h2 {
819
849
  }
820
850
 
821
851
  .section-markdown h3 {
822
- margin-top: 2.25rem;
823
- padding-top: 1.35rem;
852
+ font-size: var(--section-markdown-h3-font-size-mobile, var(--section-markdown-h3-font-size-desktop, 0.82rem));
853
+ text-align: var(--section-markdown-h3-align-mobile, var(--section-markdown-h3-align-desktop, left));
824
854
  }
825
855
 
826
856
  .section-markdown h4 {
827
- text-align: center;
857
+ font-size: var(--section-markdown-h4-font-size-mobile, var(--section-markdown-h4-font-size-desktop, clamp(0.96rem, 0.94rem + 0.08vw, 1rem)));
858
+ text-align: var(--section-markdown-h4-align-mobile, var(--section-markdown-h4-align-desktop, left));
828
859
  }
829
860
 
830
861
  .section-markdown ul,
@@ -6,7 +6,7 @@ This is a minimal site repository starter for `@janga/norna`.
6
6
 
7
7
  ```sh
8
8
  npm install
9
- npm run norna:dev
9
+ npm run dev
10
10
  ```
11
11
 
12
12
  If the standard local port is blocked, start with:
@@ -15,9 +15,9 @@ If the standard local port is blocked, start with:
15
15
  npm run dev -- --kill
16
16
  ```
17
17
 
18
- Edit site-wide theme defaults in `site/theme.md`, homepage content and section
19
- overrides in `site/content.md`, technical settings such as URL, layout, font,
20
- and locale labels in `site/config.mjs`, source images under
18
+ Edit site-wide visual defaults in `site/theme.md`, homepage content and section
19
+ overrides in `site/content.md`, technical settings such as URL and locale
20
+ labels in `site/config.mjs`, source images under
21
21
  `site/images/<section-id>/`, and static public files under `site/public/`.
22
22
 
23
23
  Use `site.basePath: '/'` for a custom domain or root-hosted site. For a GitHub
@@ -32,14 +32,10 @@ site: {
32
32
 
33
33
  In GitHub repository settings, configure Pages to build from GitHub Actions.
34
34
 
35
- The page width is configured with `layout.pageWidth`; gallery width is
36
- configured with `gallery.width`. Side margins are configured with
37
- `layout.gutter`; image height is limited with
38
- `gallery.maxAvailableHeightPercent`.
39
-
40
- The site font is configured with `typography.fontFamily` in `site/config.mjs`.
41
- Site-wide text presentation is configured with typography presets and overrides
42
- in `site/theme.md`; page and section overrides live in `site/content.md`.
35
+ Page width, side gutters, layout density, typography rhythm, image width, image
36
+ height limits, font, colors, and site-wide typography are configured in
37
+ `site/theme.md`. Page and section presentation overrides live in
38
+ `site/content.md`.
43
39
 
44
40
  Commit `package-lock.json` after the first install so GitHub Actions can use
45
41
  `npm ci`.
@@ -54,9 +50,9 @@ npm run norna:typography:show
54
50
  npm run norna:build
55
51
  ```
56
52
 
57
- This keeps gallery commands separate from repository-specific build or
58
- publishing commands in projects that embed a gallery inside a larger GitHub
59
- project. This pure starter also keeps `npm run build` as an alias for
53
+ This keeps Norna commands separate from repository-specific build or publishing
54
+ commands in projects that embed a Norna site inside a larger GitHub project.
55
+ This pure starter also keeps `npm run build` as an alias for
60
56
  `npm run norna:build`.
61
57
 
62
58
  Use `npm run norna:engine:version` to inspect the installed engine and
@@ -14,8 +14,8 @@
14
14
  "norna:config:check": "norna config:check",
15
15
  "norna:content:check": "norna content:check",
16
16
  "norna:sync": "norna content:sync",
17
- "norna:typography:presets": "norna typography:presets",
18
- "norna:typography:show": "norna typography:show",
17
+ "norna:typography:presets": "norna typography presets",
18
+ "norna:typography:show": "norna typography show",
19
19
  "norna:public": "norna site:public",
20
20
  "norna:images": "norna images",
21
21
  "norna:build": "norna build",
@@ -3,27 +3,6 @@ export default {
3
3
  url: 'https://example.com/',
4
4
  basePath: '/',
5
5
  },
6
- layout: {
7
- pageWidth: '1180px',
8
- gutter: {
9
- desktop: 'clamp(1.25rem, 4vw, 3rem)',
10
- mobile: '1rem',
11
- },
12
- },
13
- gallery: {
14
- width: '900px',
15
- maxAvailableWidthPercent: {
16
- desktop: 100,
17
- mobile: 100,
18
- },
19
- maxAvailableHeightPercent: {
20
- desktop: 74,
21
- mobile: 68,
22
- },
23
- },
24
- typography: {
25
- fontFamily: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
26
- },
27
6
  navigation: {
28
7
  smoothScroll: {
29
8
  enabled: true,
@@ -37,11 +16,11 @@ export default {
37
16
  labels: {
38
17
  skipToContent: 'Skip to content',
39
18
  sectionNavigation: 'Sections',
40
- gallery: 'Gallery',
19
+ gallery: 'Images',
41
20
  },
42
21
  },
43
22
  footer: {
44
- copyrightMessage: '(c) Example Artist.',
23
+ copyrightMessage: '(c) Example Owner.',
45
24
  buildInfo: {
46
25
  enabled: true,
47
26
  text: 'Built',
@@ -54,7 +33,7 @@ export default {
54
33
  },
55
34
  },
56
35
  github: {
57
- repo: 'owner/example-gallery',
36
+ repo: 'owner/example-site',
58
37
  branch: 'main',
59
38
  pagesWorkflow: 'Deploy to GitHub Pages',
60
39
  },