@janga/norna 0.7.16 → 0.7.17

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 (200) hide show
  1. package/README.md +6 -6
  2. package/astro.config.mjs +35 -3
  3. package/docs/README.md +10 -10
  4. package/docs/commands.md +2 -2
  5. package/docs/content.md +82 -102
  6. package/docs/design/navigation-and-theme-plan.md +115 -0
  7. package/docs/design/norna-diagram-design.md +9 -9
  8. package/docs/design/site-examples-structure.md +9 -9
  9. package/docs/engine-development.md +9 -9
  10. package/docs/getting-started.md +20 -15
  11. package/docs/images-and-metadata.md +10 -9
  12. package/docs/pages.md +134 -0
  13. package/docs/public-files.md +49 -36
  14. package/docs/publishing.md +19 -3
  15. package/docs/site-files.md +29 -28
  16. package/docs/sitewide-content.md +20 -36
  17. package/docs/theme.md +20 -20
  18. package/docs/typography.md +10 -10
  19. package/examples/README.md +2 -2
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  21. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  27. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  28. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  29. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  30. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +3 -4
  31. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  34. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  35. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  41. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  42. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  43. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  44. package/examples/complete-sites/dog-shelter-single-page/site/content.md +5 -2
  45. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  46. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  47. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +185 -88
  49. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  50. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  51. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  52. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  53. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  54. package/examples/feature-demos/media-and-surfaces/site/content.md +4 -7
  55. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  56. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +7 -6
  57. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  58. package/examples/feature-demos/sitewide-content/README.md +3 -3
  59. package/examples/feature-demos/sitewide-content/site/content.md +8 -7
  60. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  61. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  62. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  63. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  64. package/examples/feature-demos/theme-presets/README.md +1 -1
  65. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +60 -60
  66. package/examples/feature-demos/theme-presets/site/content.md +4 -7
  67. package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/content.md +4 -7
  68. package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/content.md +4 -7
  69. package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/content.md +4 -7
  70. package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/content.md +4 -7
  71. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +1 -10
  72. package/fixtures/basic/site/content.md +5 -6
  73. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  74. package/fixtures/content-model-v2/site/content.md +11 -13
  75. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  76. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  77. package/fixtures/nested-pages/site/config.yaml +1 -0
  78. package/fixtures/nested-pages/site/content.md +15 -0
  79. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  80. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  81. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  82. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +14 -0
  83. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  84. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +3 -0
  85. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  86. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  87. package/fixtures/nested-pages/site/theme.yaml +3 -0
  88. package/package.json +7 -4
  89. package/schemas/config.schema.json +5 -5
  90. package/schemas/content-frontmatter.schema.json +28 -63
  91. package/schemas/manifest.json +1 -0
  92. package/schemas/sitewide-content.schema.json +58 -132
  93. package/schemas/theme.schema.json +396 -86
  94. package/scripts/check-config.mjs +6 -5
  95. package/scripts/deploy-site.mjs +6 -8
  96. package/scripts/doctor.mjs +2 -2
  97. package/scripts/generate-images.mjs +11 -18
  98. package/scripts/generate-schemas.mjs +3 -217
  99. package/scripts/lib/base-path-redirect.mjs +9 -0
  100. package/scripts/lib/editor-language-service.mjs +126 -66
  101. package/scripts/lib/heading-ids.mjs +161 -0
  102. package/scripts/lib/navigation-model.mjs +51 -0
  103. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  104. package/scripts/lib/page-model.mjs +83 -0
  105. package/scripts/lib/project-config.mjs +28 -0
  106. package/scripts/lib/schema-definitions.mjs +41 -46
  107. package/scripts/lib/schema-editor-metadata.mjs +475 -0
  108. package/scripts/lib/schema-value-definitions.mjs +6 -6
  109. package/scripts/lib/site-content.mjs +124 -87
  110. package/scripts/lib/site-paths.mjs +2 -2
  111. package/scripts/lib/theme-config.mjs +9 -16
  112. package/scripts/lib/theme-presets.mjs +9 -1
  113. package/scripts/release.mjs +1 -1
  114. package/scripts/show-typography.mjs +26 -20
  115. package/scripts/sync-content-sections.mjs +106 -65
  116. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +7 -24
  117. package/scripts/test-client-javascript.mjs +39 -42
  118. package/scripts/test-content-check.mjs +138 -54
  119. package/scripts/test-content-model-v2.mjs +445 -228
  120. package/scripts/test-documentation.mjs +6 -1
  121. package/scripts/test-editor-language-service.mjs +168 -24
  122. package/scripts/test-engine-commands.mjs +2 -1
  123. package/scripts/test-heading-ids.mjs +68 -0
  124. package/scripts/test-navigation-model.mjs +63 -0
  125. package/scripts/test-nested-pages.mjs +110 -0
  126. package/scripts/test-package-check.mjs +31 -40
  127. package/scripts/test-page-model.mjs +80 -0
  128. package/scripts/test-project-config.mjs +25 -1
  129. package/scripts/test-schemas.mjs +66 -14
  130. package/scripts/test-theme-presets.mjs +33 -24
  131. package/src/components/NavigationPageTree.astro +68 -0
  132. package/src/components/SectionNavigationScript.astro +110 -5
  133. package/src/components/SiteBreadcrumbs.astro +29 -0
  134. package/src/components/SiteNavigation.astro +65 -69
  135. package/src/components/SitePage.astro +70 -46
  136. package/src/components/SiteSection.astro +27 -20
  137. package/src/content.config.ts +9 -5
  138. package/src/layouts/BaseLayout.astro +11 -20
  139. package/src/lib/pageThemes.ts +42 -0
  140. package/src/lib/sectionContent.ts +109 -106
  141. package/src/lib/siteNavigation.ts +48 -0
  142. package/src/lib/sitePages.ts +85 -38
  143. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  144. package/src/styles/global.css +373 -55
  145. package/starters/basic/README.md +5 -7
  146. package/starters/basic/site/content.md +7 -4
  147. package/starters/basic/site/sitewide-content.yaml +1 -10
  148. package/starters/project/README.md +8 -8
  149. package/starters/project/site/content.md +5 -4
  150. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  151. package/starters/project/site/sitewide-content.yaml +1 -10
  152. package/docs/routes.md +0 -121
  153. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  154. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  155. package/scripts/lib/route-model.mjs +0 -17
  156. package/scripts/test-route-model.mjs +0 -32
  157. package/src/lib/routeThemes.ts +0 -41
  158. /package/examples/complete-sites/dog-shelter-multi-page/site/images/{intro/shelter-dog.jpg → shelter-dog.jpg} +0 -0
  159. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  160. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  161. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  162. /package/examples/complete-sites/dog-shelter-single-page/site/images/{you-can-help/foster-dog.jpg → foster-dog.jpg} +0 -0
  163. /package/examples/complete-sites/dog-shelter-single-page/site/images/{what-we-do/rescue-dog.jpg → rescue-dog.jpg} +0 -0
  164. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  165. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  166. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  167. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  168. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  169. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  170. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  171. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  172. /package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/theme.yaml +0 -0
  173. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/puppy-portrait.jpg +0 -0
  174. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/shepherd-portrait.jpg +0 -0
  175. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/white-puppy.jpg +0 -0
  176. /package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/theme.yaml +0 -0
  177. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/black-schnauzer.jpg +0 -0
  178. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/retriever-portrait.jpg +0 -0
  179. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/schnauzer-portrait.jpg +0 -0
  180. /package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/theme.yaml +0 -0
  181. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/boxer-portrait.jpg +0 -0
  182. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grass-puppy.jpg +0 -0
  183. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grey-street-dog.jpg +0 -0
  184. /package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/theme.yaml +0 -0
  185. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/black-pedigree.jpg +0 -0
  186. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-grass.jpg +0 -0
  187. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-wide.jpg +0 -0
  188. /package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/theme.yaml +0 -0
  189. /package/fixtures/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
  190. /package/fixtures/content-model-v2/site/images/{card-section/adopt.svg → adopt.svg} +0 -0
  191. /package/fixtures/content-model-v2/site/images/{intro/detail.jpg → detail.jpg} +0 -0
  192. /package/fixtures/content-model-v2/site/images/{intro/duplicate.jpg → duplicate.jpg} +0 -0
  193. /package/fixtures/content-model-v2/site/images/{card-section/foster.svg → foster.svg} +0 -0
  194. /package/fixtures/content-model-v2/site/images/{intro/hero.jpg → hero.jpg} +0 -0
  195. /package/fixtures/content-model-v2/site/images/{plain/image.jpg → image.jpg} +0 -0
  196. /package/fixtures/content-model-v2/site/images/{timed/slide-one.jpg → slide-one.jpg} +0 -0
  197. /package/fixtures/content-model-v2/site/images/{timed/slide-three.jpg → slide-three.jpg} +0 -0
  198. /package/fixtures/content-model-v2/site/images/{timed/slide-two.jpg → slide-two.jpg} +0 -0
  199. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  200. /package/starters/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
@@ -0,0 +1,475 @@
1
+ import { documentationLink } from './documentation-links.mjs';
2
+ import { getSchemaValueDefinition } from './schema-value-definitions.mjs';
3
+
4
+ const yamlExample = (source) => `\`\`\`yaml\n${source}\n\`\`\``;
5
+
6
+ const expandSchemaVariants = (schemas) => schemas.flatMap((schema) => [
7
+ schema,
8
+ ...(schema?.allOf ?? []),
9
+ ...(schema?.anyOf ?? []),
10
+ ...(schema?.oneOf ?? []),
11
+ ]);
12
+
13
+ const schemaProperties = (schema, propertyPath) => {
14
+ let current = [schema];
15
+ for (const name of propertyPath.split('.')) {
16
+ current = expandSchemaVariants(current).flatMap((candidate) => {
17
+ if (name === '[]') return candidate?.items ? [candidate.items] : [];
18
+ if (name === '*') {
19
+ return candidate?.additionalProperties && typeof candidate.additionalProperties === 'object'
20
+ ? [candidate.additionalProperties]
21
+ : [];
22
+ }
23
+ return candidate?.properties?.[name] ? [candidate.properties[name]] : [];
24
+ });
25
+ if (current.length === 0) throw new Error(`Generated schema has no property at ${propertyPath}.`);
26
+ }
27
+ return current;
28
+ };
29
+
30
+ const schemaProperty = (schema, propertyPath) => schemaProperties(schema, propertyPath)[0];
31
+
32
+ const addHelp = (schema, propertyPath, paragraphs, examples = []) => {
33
+ for (const property of schemaProperties(schema, propertyPath)) {
34
+ property.markdownDescription = paragraphs.join('\n\n');
35
+ if (examples.length > 0) property.examples = examples;
36
+ }
37
+ };
38
+
39
+ const addFieldHelp = (schema, propertyPath, example, file, anchor, examples = []) => {
40
+ const property = schemaProperty(schema, propertyPath);
41
+ addHelp(schema, propertyPath, [
42
+ yamlExample(example),
43
+ property.description,
44
+ documentationLink('Read the relevant reference', file, anchor),
45
+ ], examples);
46
+ };
47
+
48
+ const addSnippets = (schema, propertyPath, snippets) => {
49
+ for (const property of schemaProperties(schema, propertyPath)) {
50
+ property.defaultSnippets = snippets;
51
+ }
52
+ };
53
+
54
+ const schemaSnippet = ({ label, body, description, file, anchor }) => ({
55
+ label,
56
+ markdownDescription: [
57
+ description,
58
+ documentationLink('Read the relevant reference', file, anchor),
59
+ ].join('\n\n'),
60
+ body,
61
+ });
62
+
63
+ const getLiteralValues = (schema) => {
64
+ if (Array.isArray(schema.enum)) return schema.enum;
65
+ if (
66
+ Array.isArray(schema.anyOf)
67
+ && schema.anyOf.length > 0
68
+ && schema.anyOf.every((candidate) => Object.hasOwn(candidate, 'const'))
69
+ ) {
70
+ return schema.anyOf.map((candidate) => candidate.const);
71
+ }
72
+ return null;
73
+ };
74
+
75
+ const addValueDescriptions = (schema) => {
76
+ if (!schema || typeof schema !== 'object') return;
77
+ if (Array.isArray(schema)) {
78
+ for (const value of schema) addValueDescriptions(value);
79
+ return;
80
+ }
81
+
82
+ const values = getLiteralValues(schema);
83
+ const definition = values ? getSchemaValueDefinition(values) : null;
84
+ if (definition) {
85
+ delete schema.enum;
86
+ delete schema.anyOf;
87
+ schema.oneOf = values.map((value) => ({
88
+ const: value,
89
+ title: definition.options[value].title,
90
+ description: definition.options[value].description,
91
+ }));
92
+ }
93
+
94
+ for (const value of Object.values(schema)) addValueDescriptions(value);
95
+ };
96
+
97
+ const addLanguageSuggestions = (jsonSchema) => {
98
+ const language = jsonSchema.properties?.language;
99
+ if (!language) throw new Error('Generated config schema has no language property.');
100
+ const definition = getSchemaValueDefinition(['en', 'sv']);
101
+ language.default = 'en';
102
+ language.examples = ['en', 'sv', 'en-GB', 'sv-SE'];
103
+ language.oneOf = [
104
+ ...definition.values.map((value) => ({
105
+ const: value,
106
+ title: definition.options[value].title,
107
+ description: definition.options[value].description,
108
+ })),
109
+ {
110
+ type: 'string',
111
+ pattern: '^(?:en|sv)-[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$',
112
+ title: 'Regional language tag',
113
+ description: 'Use an English or Swedish regional language tag such as en-GB or sv-SE.',
114
+ },
115
+ ];
116
+ };
117
+
118
+ const addSitewideLogoHelp = (jsonSchema) => {
119
+ const logo = jsonSchema.properties?.logo;
120
+ if (!logo) throw new Error('Generated site-wide schema has no logo property.');
121
+
122
+ logo.markdownDescription = [
123
+ yamlExample('logo:\n height: 2rem'),
124
+ '`logo` optionally overrides the displayed height of the convention-based logo file. The logo links home, and its alternative text comes from the homepage Markdown H1. This setting does not enable or select the file.',
125
+ documentationLink('Navigation logo filenames and placement', 'public-files.md', 'navigation-logo'),
126
+ ].join('\n\n');
127
+ logo.properties.height.examples = ['2rem'];
128
+ addFieldHelp(
129
+ jsonSchema,
130
+ 'logo.height',
131
+ 'logo:\n height: 2rem',
132
+ 'public-files.md',
133
+ 'navigation-logo',
134
+ ['2rem'],
135
+ );
136
+ };
137
+
138
+ const addConfigHelp = (jsonSchema) => {
139
+ jsonSchema.markdownDescription = [
140
+ yamlExample('url: https://example.com/'),
141
+ '`config.yaml` contains the few technical settings shared by the complete site.',
142
+ documentationLink('Configuration reference', 'configuration.md'),
143
+ ].join('\n\n');
144
+ addHelp(jsonSchema, 'url', [
145
+ yamlExample('url: https://example.com/'),
146
+ 'The absolute public URL. Its path also determines the base path for generated links and assets.',
147
+ documentationLink('Public URL reference', 'configuration.md', 'url'),
148
+ ], ['https://example.com/', 'https://owner.github.io/repository-name/']);
149
+ addHelp(jsonSchema, 'language', [
150
+ yamlExample('language: en-GB'),
151
+ 'Sets the page language and selects Norna\'s built-in English or Swedish interface text. The default is `en`.',
152
+ documentationLink('Language reference', 'configuration.md', 'language'),
153
+ ], ['en', 'sv', 'en-GB', 'sv-SE']);
154
+ addHelp(jsonSchema, 'scrollBehavior', [
155
+ yamlExample('scrollBehavior: smooth'),
156
+ 'Controls same-page anchor movement. `instant` is the default; `smooth` uses the browser\'s native smooth scrolling.',
157
+ documentationLink('Scroll behavior reference', 'configuration.md', 'scrollbehavior'),
158
+ ], ['instant', 'smooth']);
159
+ };
160
+
161
+ const addThemeHelp = (jsonSchema) => {
162
+ jsonSchema.markdownDescription = [
163
+ yamlExample('preset: documentation'),
164
+ 'Choose one complete preset first. Add focused overrides only when the site needs to differ from it.',
165
+ documentationLink('Theme reference', 'theme.md'),
166
+ ].join('\n\n');
167
+ addHelp(jsonSchema, 'preset', [
168
+ yamlExample('preset: documentation'),
169
+ 'Selects coordinated navigation, layout, image sizing, typography, palette and section surfaces. This is the normal starting point for a theme.',
170
+ documentationLink('Compare theme presets', 'theme.md', 'theme-presets'),
171
+ ]);
172
+ addHelp(jsonSchema, 'navigation', [
173
+ yamlExample('navigation:\n mode: automatic'),
174
+ 'Sets the site-wide navigation model. Only the root theme may override this preset value.',
175
+ documentationLink('Theme reference', 'theme.md'),
176
+ ]);
177
+ addFieldHelp(
178
+ jsonSchema,
179
+ 'navigation.mode',
180
+ 'navigation:\n mode: automatic',
181
+ 'theme.md',
182
+ undefined,
183
+ ['automatic', 'sections', 'top', 'tree'],
184
+ );
185
+ addHelp(jsonSchema, 'layout', [
186
+ yamlExample('layout:\n density: compact\n pageWidth: 72rem'),
187
+ 'Overrides page width, gutters and structural spacing after the selected preset.',
188
+ documentationLink('Layout reference', 'theme.md', 'layout'),
189
+ ]);
190
+ addHelp(jsonSchema, 'images', [
191
+ yamlExample('images:\n width: 900px\n maxAvailableHeightPercent: 74'),
192
+ 'Overrides the available width and height used by Norna-managed images.',
193
+ documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
194
+ ]);
195
+ addHelp(jsonSchema, 'typography', [
196
+ yamlExample('typography:\n profile: reading\n rhythm: normal'),
197
+ 'Overrides the preset\'s font stack, typography profile, rhythm or individual text settings.',
198
+ documentationLink('Typography configuration reference', 'typography.md', 'configuration-shape'),
199
+ ]);
200
+ addHelp(jsonSchema, 'palette', [
201
+ yamlExample('palette: paper'),
202
+ 'Chooses a coordinated color system for the page frame, navigation, footer and section surfaces.',
203
+ documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
204
+ ], ['dark', 'light', 'paper']);
205
+ addHelp(jsonSchema, 'sectionSurfaces', [
206
+ yamlExample('sectionSurfaces: [base, soft, emphasis]'),
207
+ 'Lists one to three semantic surfaces. Norna cycles through them in section order; omit this field to keep the preset sequence.',
208
+ documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
209
+ ], [['base'], ['base', 'soft'], ['base', 'soft', 'emphasis']]);
210
+
211
+ const layoutFields = [
212
+ ['layout.density', 'layout:\n density: compact'],
213
+ ['layout.pageWidth', 'layout:\n pageWidth: 72rem'],
214
+ ['layout.gutter', 'layout:\n gutter:\n desktop: clamp(1.25rem, 4vw, 3rem)\n mobile: 1rem'],
215
+ ['layout.gutter.desktop', 'layout:\n gutter:\n desktop: clamp(1.25rem, 4vw, 3rem)'],
216
+ ['layout.gutter.mobile', 'layout:\n gutter:\n mobile: 1rem'],
217
+ ['layout.spacing', 'layout:\n spacing:\n sectionGap: 2rem'],
218
+ ];
219
+ for (const [propertyPath, example] of layoutFields) {
220
+ addFieldHelp(jsonSchema, propertyPath, example, 'theme.md', 'layout');
221
+ }
222
+
223
+ const spacingExamples = {
224
+ blockGap: '1.5em',
225
+ finalSectionBottom: '2rem',
226
+ firstSectionTop: '1.5rem',
227
+ headingToBlock: '0.75em',
228
+ imageGap: '1rem',
229
+ sectionGap: '2rem',
230
+ };
231
+ for (const [name, value] of Object.entries(spacingExamples)) {
232
+ const prefix = `layout:\n spacing:\n ${name}:`;
233
+ addFieldHelp(jsonSchema, `layout.spacing.${name}`, `${prefix} ${value}`, 'theme.md', 'layout');
234
+ addFieldHelp(jsonSchema, `layout.spacing.${name}.desktop`, `${prefix}\n desktop: ${value}`, 'theme.md', 'layout');
235
+ addFieldHelp(jsonSchema, `layout.spacing.${name}.mobile`, `${prefix}\n mobile: ${value}`, 'theme.md', 'layout');
236
+ addSnippets(jsonSchema, `layout.spacing.${name}`, [schemaSnippet({
237
+ label: 'Responsive spacing',
238
+ body: {
239
+ desktop: `\${1:${value}}`,
240
+ mobile: `\${2:${value}}`,
241
+ },
242
+ description: 'Set separate spacing values for wider and narrow screens.',
243
+ file: 'theme.md',
244
+ anchor: 'layout',
245
+ })]);
246
+ }
247
+ addSnippets(jsonSchema, 'layout.gutter', [schemaSnippet({
248
+ label: 'Responsive page gutter',
249
+ body: {
250
+ desktop: '${1:clamp(1.25rem, 4vw, 3rem)}',
251
+ mobile: '${2:1rem}',
252
+ },
253
+ description: 'Set separate page gutters for wider and narrow screens.',
254
+ file: 'theme.md',
255
+ anchor: 'layout',
256
+ })]);
257
+ addSnippets(jsonSchema, 'layout.spacing', [schemaSnippet({
258
+ label: 'Layout spacing overrides',
259
+ body: {
260
+ sectionGap: {
261
+ desktop: '${1:2rem}',
262
+ mobile: '${2:1.5rem}',
263
+ },
264
+ headingToBlock: '${3:0.75em}',
265
+ blockGap: '${4:1.5em}',
266
+ },
267
+ description: 'Start with the structural spacing values most commonly adjusted together.',
268
+ file: 'theme.md',
269
+ anchor: 'layout',
270
+ })]);
271
+
272
+ const imageFields = [
273
+ ['images.width', 'images:\n width: 900px'],
274
+ ['images.maxAvailableWidthPercent', 'images:\n maxAvailableWidthPercent:\n desktop: 100\n mobile: 100'],
275
+ ['images.maxAvailableWidthPercent.desktop', 'images:\n maxAvailableWidthPercent:\n desktop: 100'],
276
+ ['images.maxAvailableWidthPercent.mobile', 'images:\n maxAvailableWidthPercent:\n mobile: 100'],
277
+ ['images.maxAvailableHeightPercent', 'images:\n maxAvailableHeightPercent:\n desktop: 74\n mobile: 68'],
278
+ ['images.maxAvailableHeightPercent.desktop', 'images:\n maxAvailableHeightPercent:\n desktop: 74'],
279
+ ['images.maxAvailableHeightPercent.mobile', 'images:\n maxAvailableHeightPercent:\n mobile: 68'],
280
+ ];
281
+ for (const [propertyPath, example] of imageFields) {
282
+ addFieldHelp(jsonSchema, propertyPath, example, 'theme.md', 'image-sizing');
283
+ }
284
+ for (const [propertyPath, desktop, mobile] of [
285
+ ['images.maxAvailableWidthPercent', 100, 100],
286
+ ['images.maxAvailableHeightPercent', 74, 68],
287
+ ]) {
288
+ addSnippets(jsonSchema, propertyPath, [schemaSnippet({
289
+ label: 'Responsive image limit',
290
+ body: {
291
+ desktop: `\${1:${desktop}}`,
292
+ mobile: `\${2:${mobile}}`,
293
+ },
294
+ description: 'Set separate managed-image limits for wider and narrow screens.',
295
+ file: 'theme.md',
296
+ anchor: 'image-sizing',
297
+ })]);
298
+ }
299
+
300
+ const typographyFields = [
301
+ ['typography.fontFamily', 'typography:\n fontFamily: "Arial, sans-serif"'],
302
+ ['typography.profile', 'typography:\n profile: reading'],
303
+ ['typography.rhythm', 'typography:\n rhythm: normal'],
304
+ ['typography.overrides', 'typography:\n overrides:\n body:\n width: narrow'],
305
+ ['typography.overrides.headings', 'typography:\n overrides:\n headings:\n h2:\n size: medium'],
306
+ ['typography.overrides.body', 'typography:\n overrides:\n body:\n width: narrow'],
307
+ ['typography.overrides.caption', 'typography:\n overrides:\n caption:\n size: small'],
308
+ ];
309
+ for (const [propertyPath, example] of typographyFields) {
310
+ addFieldHelp(jsonSchema, propertyPath, example, 'typography.md', 'configuration-shape');
311
+ }
312
+
313
+ const textOverrideHelp = (propertyPath, prefix, includeHeadingFields = false) => {
314
+ const fields = [
315
+ ['align', `${prefix}\n align:\n desktop: left\n mobile: left`],
316
+ ['align.desktop', `${prefix}\n align:\n desktop: left`],
317
+ ['align.mobile', `${prefix}\n align:\n mobile: left`],
318
+ ['size', `${prefix}\n size: medium`],
319
+ ['lineHeight', `${prefix}\n lineHeight: 1.5`],
320
+ ];
321
+ if (includeHeadingFields) {
322
+ fields.push(
323
+ ['weight', `${prefix}\n weight: 600`],
324
+ ['spacingBefore', `${prefix}\n spacingBefore: 1.5em`],
325
+ ['spacingAfter', `${prefix}\n spacingAfter: 0.5em`],
326
+ );
327
+ }
328
+ for (const [name, example] of fields) {
329
+ addFieldHelp(jsonSchema, `${propertyPath}.${name}`, example, 'typography.md', 'configuration-shape');
330
+ }
331
+ addSnippets(jsonSchema, `${propertyPath}.align`, [schemaSnippet({
332
+ label: 'Responsive text alignment',
333
+ body: {
334
+ desktop: '${1:left}',
335
+ mobile: '${2:left}',
336
+ },
337
+ description: 'Set separate text alignment for wider and narrow screens.',
338
+ file: 'typography.md',
339
+ anchor: 'configuration-shape',
340
+ })]);
341
+ };
342
+ for (const level of ['h1', 'h2', 'h3', 'h4']) {
343
+ const propertyPath = `typography.overrides.headings.${level}`;
344
+ const prefix = `typography:\n overrides:\n headings:\n ${level}:`;
345
+ addFieldHelp(jsonSchema, propertyPath, `${prefix}\n size: medium`, 'typography.md', 'configuration-shape');
346
+ textOverrideHelp(propertyPath, `${prefix}\n `, true);
347
+ }
348
+ textOverrideHelp('typography.overrides.body', 'typography:\n overrides:\n body:');
349
+ addFieldHelp(jsonSchema, 'typography.overrides.body.width', 'typography:\n overrides:\n body:\n width: narrow', 'typography.md', 'configuration-shape');
350
+ addFieldHelp(jsonSchema, 'typography.overrides.body.paragraphSpacing', 'typography:\n overrides:\n body:\n paragraphSpacing: 1em', 'typography.md', 'configuration-shape');
351
+ textOverrideHelp('typography.overrides.caption', 'typography:\n overrides:\n caption:');
352
+ addFieldHelp(jsonSchema, 'typography.overrides.caption.spacingBefore', 'typography:\n overrides:\n caption:\n spacingBefore: 0.5em', 'typography.md', 'configuration-shape');
353
+ addSnippets(jsonSchema, 'typography.overrides', [schemaSnippet({
354
+ label: 'Typography overrides',
355
+ body: {
356
+ headings: {
357
+ h2: {
358
+ size: '${1:medium}',
359
+ weight: '${2:600}',
360
+ },
361
+ },
362
+ body: {
363
+ width: '${3:narrow}',
364
+ lineHeight: '${4:1.55}',
365
+ },
366
+ },
367
+ description: 'Start a focused set of heading and body-text overrides.',
368
+ file: 'typography.md',
369
+ anchor: 'configuration-shape',
370
+ })]);
371
+ };
372
+
373
+ const addSitewideHelp = (jsonSchema) => {
374
+ jsonSchema.markdownDescription = [
375
+ yamlExample('footer:\n copyrightMessage: Copyright Example Owner.'),
376
+ '`sitewide-content.yaml` optionally defines banners, footer content, and logo display settings shared by every page.',
377
+ documentationLink('Site-wide content reference', 'sitewide-content.md'),
378
+ ].join('\n\n');
379
+ addSitewideLogoHelp(jsonSchema);
380
+ addHelp(jsonSchema, 'banners', [
381
+ yamlExample('banners:\n - id: project-status\n tone: warning\n title: Experimental code\n text: Not for production use.'),
382
+ 'Adds dismissible notices above page content. List order controls their presentation order; each `id` must be unique.',
383
+ documentationLink('Banner reference', 'sitewide-content.md', 'banners'),
384
+ ]);
385
+ const bannerItem = jsonSchema.properties?.banners?.items;
386
+ if (!bannerItem) throw new Error('Generated site-wide schema has no banner item definition.');
387
+ const bannerItemHelp = [
388
+ 'A `warning` is a dismissible site-wide notice shown above page content. Use it for important temporary information visitors should notice, rather than ordinary page content.',
389
+ documentationLink('Banner reference', 'sitewide-content.md', 'banners'),
390
+ ].join('\n\n');
391
+ bannerItem.title = 'Warning banner';
392
+ bannerItem.markdownDescription = bannerItemHelp;
393
+ bannerItem.defaultSnippets = [{
394
+ label: 'Warning banner',
395
+ markdownDescription: bannerItemHelp,
396
+ body: {
397
+ id: '${1:project-status}',
398
+ tone: 'warning',
399
+ title: '${2:Important notice}',
400
+ text: '${3:Brief explanation.}',
401
+ },
402
+ }];
403
+ addHelp(jsonSchema, 'footer', [
404
+ yamlExample('footer:\n copyrightMessage: Copyright Example Owner.'),
405
+ 'Adds shared footer content. Generated build information is optional.',
406
+ documentationLink('Footer reference', 'sitewide-content.md', 'footer'),
407
+ ]);
408
+
409
+ const bannerFields = [
410
+ ['banners.[].id', 'banners:\n - id: project-status'],
411
+ ['banners.[].tone', 'banners:\n - tone: warning'],
412
+ ['banners.[].visible', 'banners:\n - visible:\n from: "2026-01-01"\n until: "2026-02-01"'],
413
+ ['banners.[].visible.from', 'banners:\n - visible:\n from: "2026-01-01"'],
414
+ ['banners.[].visible.until', 'banners:\n - visible:\n until: "2026-02-01"'],
415
+ ['banners.[].title', 'banners:\n - title: Important notice'],
416
+ ['banners.[].text', 'banners:\n - text: Brief explanation.'],
417
+ ];
418
+ for (const [propertyPath, example] of bannerFields) {
419
+ addFieldHelp(jsonSchema, propertyPath, example, 'sitewide-content.md', 'banners');
420
+ }
421
+ addSnippets(jsonSchema, 'banners.[].visible', [schemaSnippet({
422
+ label: 'Visibility window',
423
+ body: {
424
+ from: '${1:2026-01-01}',
425
+ until: '${2:2026-02-01}',
426
+ },
427
+ description: 'Show the banner from the first date up to, but not including, the second date.',
428
+ file: 'sitewide-content.md',
429
+ anchor: 'banners',
430
+ })]);
431
+
432
+ const footerFields = [
433
+ ['footer.copyrightMessage', 'footer:\n copyrightMessage: Copyright Example Owner.'],
434
+ ['footer.buildInfo', 'footer:\n buildInfo: true'],
435
+ ];
436
+ for (const [propertyPath, example] of footerFields) {
437
+ addFieldHelp(jsonSchema, propertyPath, example, 'sitewide-content.md', 'footer');
438
+ }
439
+ };
440
+
441
+ const addContentHelp = (jsonSchema) => {
442
+ jsonSchema.markdownDescription = [
443
+ yamlExample('---\npage:\n description: About this project.\n---'),
444
+ 'Content frontmatter is optional. Write the page title as the single Markdown H1; use frontmatter only for optional metadata or navigation settings.',
445
+ documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
446
+ ].join('\n\n');
447
+ addHelp(jsonSchema, 'page', [
448
+ yamlExample('page:\n description: About this project.'),
449
+ 'Contains optional metadata for the current homepage or additional page. The Markdown H1 supplies its title.',
450
+ documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
451
+ ]);
452
+ addHelp(jsonSchema, 'page.description', [
453
+ yamlExample('page:\n description: About this project.'),
454
+ 'An optional page-specific description rendered as `<meta name="description">`. It is not visible in page content.',
455
+ documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
456
+ ], ['About this project.']);
457
+ addHelp(jsonSchema, 'navigation', [
458
+ yamlExample('navigation:\n listed: false'),
459
+ 'Optionally excludes an additional page from site navigation without removing its public URL.',
460
+ documentationLink('Site navigation reference', 'pages.md', 'navigation'),
461
+ ]);
462
+ addFieldHelp(jsonSchema, 'navigation.listed', 'navigation:\n listed: false', 'pages.md', 'navigation', [true, false]);
463
+ };
464
+
465
+ export const applySchemaEditorMetadata = (filename, jsonSchema) => {
466
+ addValueDescriptions(jsonSchema);
467
+ if (filename === 'config.schema.json') {
468
+ addLanguageSuggestions(jsonSchema);
469
+ addConfigHelp(jsonSchema);
470
+ }
471
+ if (filename === 'theme.schema.json') addThemeHelp(jsonSchema);
472
+ if (filename === 'sitewide-content.schema.json') addSitewideHelp(jsonSchema);
473
+ if (filename === 'content-frontmatter.schema.json') addContentHelp(jsonSchema);
474
+ return jsonSchema;
475
+ };
@@ -18,6 +18,12 @@ export const schemaValueDefinitions = Object.freeze([
18
18
  instant: option('Instant', 'Jump directly to anchors without animated scrolling. This is the default.'),
19
19
  smooth: option('Browser smooth', 'Use the browser\'s native smooth scrolling for anchor navigation.'),
20
20
  }),
21
+ definition(['automatic', 'sections', 'top', 'tree'], {
22
+ automatic: option('Automatic', 'Choose sections for one page, top navigation for a shallow multi-page site, and tree navigation for deeper content.'),
23
+ sections: option('Sections', 'Navigate the sections of the current page.'),
24
+ top: option('Top', 'Use pages in the top navigation and sections below them.'),
25
+ tree: option('Tree', 'Use a hierarchical site-wide navigation tree.'),
26
+ }),
21
27
  definition(['en', 'sv'], {
22
28
  en: option('English', 'Use Norna\'s built-in English interface text.'),
23
29
  sv: option('Swedish', 'Use Norna\'s built-in Swedish interface text.'),
@@ -68,12 +74,6 @@ export const schemaValueDefinitions = Object.freeze([
68
74
  definition(['warning'], {
69
75
  warning: option('Warning', 'Present the banner as an important warning notice.'),
70
76
  }),
71
- definition(['short', 'medium', 'long', 'full'], {
72
- short: option('Short', 'Use a compact localized date or time representation.'),
73
- medium: option('Medium', 'Use a moderately detailed localized representation.'),
74
- long: option('Long', 'Use a detailed localized representation.'),
75
- full: option('Full', 'Use the most detailed localized representation.'),
76
- }),
77
77
  ]);
78
78
 
79
79
  const valuesEqual = (left, right) => (