@janga/norna 0.7.25 → 0.7.26

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 (131) hide show
  1. package/README.md +3 -2
  2. package/astro.config.mjs +26 -12
  3. package/bin/norna-cli.mjs +12 -0
  4. package/package.json +14 -5
  5. package/schemas/card-list.schema.json +121 -0
  6. package/schemas/category.schema.json +2 -2
  7. package/schemas/config.schema.json +306 -32
  8. package/schemas/content-frontmatter.schema.json +7 -7
  9. package/schemas/image-carousel.schema.json +43 -0
  10. package/schemas/image-stack.schema.json +43 -0
  11. package/schemas/manifest.json +6 -1
  12. package/schemas/page-theme.schema.json +26 -26
  13. package/schemas/sitewide-content.schema.json +18 -18
  14. package/schemas/theme.schema.json +237 -262
  15. package/scripts/dev-local.mjs +24 -2
  16. package/scripts/generate-schemas.mjs +15 -3
  17. package/scripts/lib/category-destinations.mjs +48 -0
  18. package/scripts/lib/code-fence-metadata.mjs +14 -37
  19. package/scripts/lib/content-tabs.mjs +155 -0
  20. package/scripts/lib/details-headings.mjs +58 -0
  21. package/scripts/lib/docusaurus-migration-audit.mjs +603 -0
  22. package/scripts/lib/editor-language-service.mjs +238 -116
  23. package/scripts/lib/locale-registry.mjs +597 -0
  24. package/scripts/lib/locales/az-Latn.mjs +78 -0
  25. package/scripts/lib/locales/be.mjs +78 -0
  26. package/scripts/lib/locales/bg.mjs +78 -0
  27. package/scripts/lib/locales/bs.mjs +78 -0
  28. package/scripts/lib/locales/ca.mjs +78 -0
  29. package/scripts/lib/locales/cnr.mjs +78 -0
  30. package/scripts/lib/locales/cs.mjs +78 -0
  31. package/scripts/lib/locales/da.mjs +78 -0
  32. package/scripts/lib/locales/de.mjs +78 -0
  33. package/scripts/lib/locales/el.mjs +78 -0
  34. package/scripts/lib/locales/en.mjs +78 -0
  35. package/scripts/lib/locales/es.mjs +78 -0
  36. package/scripts/lib/locales/et.mjs +78 -0
  37. package/scripts/lib/locales/fi.mjs +78 -0
  38. package/scripts/lib/locales/fil.mjs +78 -0
  39. package/scripts/lib/locales/fr.mjs +78 -0
  40. package/scripts/lib/locales/ga.mjs +78 -0
  41. package/scripts/lib/locales/ha.mjs +78 -0
  42. package/scripts/lib/locales/hr.mjs +78 -0
  43. package/scripts/lib/locales/hu.mjs +78 -0
  44. package/scripts/lib/locales/id.mjs +78 -0
  45. package/scripts/lib/locales/is.mjs +78 -0
  46. package/scripts/lib/locales/it.mjs +78 -0
  47. package/scripts/lib/locales/jv.mjs +78 -0
  48. package/scripts/lib/locales/lb.mjs +78 -0
  49. package/scripts/lib/locales/lt.mjs +78 -0
  50. package/scripts/lib/locales/lv.mjs +78 -0
  51. package/scripts/lib/locales/mk.mjs +78 -0
  52. package/scripts/lib/locales/mt.mjs +78 -0
  53. package/scripts/lib/locales/nb.mjs +78 -0
  54. package/scripts/lib/locales/nl.mjs +78 -0
  55. package/scripts/lib/locales/nn.mjs +78 -0
  56. package/scripts/lib/locales/pcm.mjs +78 -0
  57. package/scripts/lib/locales/pl.mjs +78 -0
  58. package/scripts/lib/locales/pt.mjs +78 -0
  59. package/scripts/lib/locales/ro.mjs +78 -0
  60. package/scripts/lib/locales/ru.mjs +78 -0
  61. package/scripts/lib/locales/sk.mjs +78 -0
  62. package/scripts/lib/locales/sl.mjs +78 -0
  63. package/scripts/lib/locales/sq.mjs +78 -0
  64. package/scripts/lib/locales/sr-Cyrl.mjs +78 -0
  65. package/scripts/lib/locales/sr-Latn.mjs +78 -0
  66. package/scripts/lib/locales/sv.mjs +78 -0
  67. package/scripts/lib/locales/sw.mjs +78 -0
  68. package/scripts/lib/locales/tr.mjs +78 -0
  69. package/scripts/lib/locales/uk.mjs +78 -0
  70. package/scripts/lib/locales/vi.mjs +78 -0
  71. package/scripts/lib/markdown-links.mjs +4 -25
  72. package/scripts/lib/markdown-notes-render-plugin.mjs +75 -0
  73. package/scripts/lib/markdown-notes.mjs +165 -0
  74. package/scripts/lib/norna-markdown-blocks.mjs +32 -447
  75. package/scripts/lib/norna-markdown-render-plugin.mjs +4 -0
  76. package/scripts/lib/page-aliases.mjs +12 -1
  77. package/scripts/lib/page-markdown.mjs +63 -16
  78. package/scripts/lib/page-move-plan.mjs +22 -5
  79. package/scripts/lib/pagefind-translations.mjs +620 -0
  80. package/scripts/lib/presentation-contract.mjs +6 -1
  81. package/scripts/lib/presentation.mjs +29 -30
  82. package/scripts/lib/project-config.mjs +2 -173
  83. package/scripts/lib/schema-definitions.mjs +3 -10
  84. package/scripts/lib/schema-editor-metadata.mjs +22 -43
  85. package/scripts/lib/schema-value-definitions.mjs +4 -8
  86. package/scripts/lib/semantic-callouts.mjs +4 -2
  87. package/scripts/lib/site-content.mjs +4 -2
  88. package/scripts/lib/site-link-graph.mjs +16 -6
  89. package/scripts/lib/site-navigation-tree.mjs +0 -11
  90. package/scripts/lib/sitemap.mjs +5 -3
  91. package/scripts/lib/structured-blocks.mjs +108 -0
  92. package/scripts/lib/table-render-plugin.mjs +46 -3
  93. package/scripts/lib/table-row-headers.mjs +218 -0
  94. package/scripts/lib/theme-presets.mjs +3 -10
  95. package/scripts/lib/yaml-config.mjs +2 -6
  96. package/scripts/migrate-check.mjs +19 -0
  97. package/scripts/sync-content-sections.mjs +39 -10
  98. package/scripts/sync-site-public.mjs +7 -1
  99. package/src/components/CodeBlockCopyScript.astro +47 -5
  100. package/src/components/ContentBlocks.astro +39 -0
  101. package/src/components/ContentTabsScript.astro +60 -0
  102. package/src/components/DisplaySettings.astro +17 -19
  103. package/src/components/NavigationPageTree.astro +1 -1
  104. package/src/components/NoteNavigationScript.astro +4 -0
  105. package/src/components/PageAliasRedirect.astro +5 -4
  106. package/src/components/SearchNavigationScript.astro +4 -0
  107. package/src/components/SearchPage.astro +43 -2
  108. package/src/components/SectionNavigationScript.astro +43 -11
  109. package/src/components/SiteBreadcrumbs.astro +1 -1
  110. package/src/components/SiteNavigation.astro +29 -20
  111. package/src/components/SitePage.astro +40 -15
  112. package/src/components/SiteSection.astro +5 -81
  113. package/src/components/SiteTreeNavigation.astro +2 -2
  114. package/src/components/TableOverflowScript.astro +386 -58
  115. package/src/components/TopPageMenu.astro +47 -0
  116. package/src/components/TreeNavigationScript.astro +10 -3
  117. package/src/layouts/BaseLayout.astro +8 -12
  118. package/src/lib/navigationFollowing.ts +142 -0
  119. package/src/lib/noteNavigation.ts +50 -0
  120. package/src/lib/searchNavigation.ts +107 -0
  121. package/src/lib/sectionContent.ts +20 -90
  122. package/src/lib/siteNavigation.ts +0 -5
  123. package/src/lib/sitePages.ts +13 -2
  124. package/src/lib/table-sort-tooltip.ts +109 -0
  125. package/src/pages/[...slug].astro +16 -6
  126. package/src/styles/content.css +394 -132
  127. package/src/styles/foundations.css +9 -0
  128. package/src/styles/media.css +5 -0
  129. package/src/styles/navigation.css +63 -74
  130. package/src/styles/page-layout.css +7 -0
  131. package/src/styles/responsive.css +2 -7
@@ -87,16 +87,16 @@ const presentationPalettes = Object.freeze({
87
87
  modes: Object.freeze({
88
88
  light: createPaletteMode({
89
89
  appearance: 'light',
90
- page: { backgroundColor: '#f3f6f8', textColor: '#202932' },
90
+ page: { backgroundColor: '#e8f1f8', textColor: '#202932' },
91
91
  surfaces: {
92
- base: { backgroundColor: '#f3f6f8', textColor: '#202932' },
92
+ base: { backgroundColor: '#e8f1f8', textColor: '#202932' },
93
93
  soft: { backgroundColor: '#e4eaf0', textColor: '#202932' },
94
94
  emphasis: { backgroundColor: '#d3dee8', textColor: '#202932' },
95
95
  },
96
- frame: { backgroundColor: '#f3f6f8', textColor: '#202932' },
96
+ frame: { backgroundColor: '#e8f1f8', textColor: '#202932' },
97
97
  css: {
98
98
  surface: '#e9eef2',
99
- muted: '#5d6a76',
99
+ muted: '#56636f',
100
100
  soft: '#d5dfe7',
101
101
  accent: '#2a5877',
102
102
  border: 'rgb(32 41 50 / 16%)',
@@ -127,13 +127,13 @@ const presentationPalettes = Object.freeze({
127
127
  modes: Object.freeze({
128
128
  light: createPaletteMode({
129
129
  appearance: 'light',
130
- page: { backgroundColor: '#eef4f1', textColor: '#203631' },
130
+ page: { backgroundColor: '#e3f1eb', textColor: '#203631' },
131
131
  surfaces: {
132
- base: { backgroundColor: '#eef4f1', textColor: '#203631' },
132
+ base: { backgroundColor: '#e3f1eb', textColor: '#203631' },
133
133
  soft: { backgroundColor: '#dce9e4', textColor: '#203631' },
134
134
  emphasis: { backgroundColor: '#c8ddd5', textColor: '#203631' },
135
135
  },
136
- frame: { backgroundColor: '#eef4f1', textColor: '#203631' },
136
+ frame: { backgroundColor: '#e3f1eb', textColor: '#203631' },
137
137
  css: {
138
138
  surface: '#e3ece8',
139
139
  muted: '#536560',
@@ -167,16 +167,16 @@ const presentationPalettes = Object.freeze({
167
167
  modes: Object.freeze({
168
168
  light: createPaletteMode({
169
169
  appearance: 'light',
170
- page: { backgroundColor: '#f7f3fa', textColor: '#2e2736' },
170
+ page: { backgroundColor: '#f0e8f8', textColor: '#2e2736' },
171
171
  surfaces: {
172
- base: { backgroundColor: '#f7f3fa', textColor: '#2e2736' },
172
+ base: { backgroundColor: '#f0e8f8', textColor: '#2e2736' },
173
173
  soft: { backgroundColor: '#ebe4f1', textColor: '#2e2736' },
174
174
  emphasis: { backgroundColor: '#ddd3e8', textColor: '#2e2736' },
175
175
  },
176
- frame: { backgroundColor: '#f7f3fa', textColor: '#2e2736' },
176
+ frame: { backgroundColor: '#f0e8f8', textColor: '#2e2736' },
177
177
  css: {
178
178
  surface: '#eee8f3',
179
- muted: '#6c6273',
179
+ muted: '#655b6c',
180
180
  soft: '#ded5e7',
181
181
  accent: '#604678',
182
182
  border: 'rgb(46 39 54 / 16%)',
@@ -207,13 +207,13 @@ const presentationPalettes = Object.freeze({
207
207
  modes: Object.freeze({
208
208
  light: createPaletteMode({
209
209
  appearance: 'light',
210
- page: { backgroundColor: '#f8f5ee', textColor: '#272522' },
210
+ page: { backgroundColor: '#fdf6e3', textColor: '#272522' },
211
211
  surfaces: {
212
- base: { backgroundColor: '#f8f5ee', textColor: '#272522' },
212
+ base: { backgroundColor: '#fdf6e3', textColor: '#272522' },
213
213
  soft: { backgroundColor: '#ebe5d9', textColor: '#272522' },
214
214
  emphasis: { backgroundColor: '#ded4c5', textColor: '#272522' },
215
215
  },
216
- frame: { backgroundColor: '#f8f5ee', textColor: '#272522' },
216
+ frame: { backgroundColor: '#fdf6e3', textColor: '#272522' },
217
217
  css: {
218
218
  surface: '#f0ebe1',
219
219
  muted: '#6b655b',
@@ -247,13 +247,13 @@ const presentationPalettes = Object.freeze({
247
247
  modes: Object.freeze({
248
248
  light: createPaletteMode({
249
249
  appearance: 'light',
250
- page: { backgroundColor: '#f7efd9', textColor: '#34291c' },
250
+ page: { backgroundColor: '#fbf1c7', textColor: '#34291c' },
251
251
  surfaces: {
252
- base: { backgroundColor: '#f7efd9', textColor: '#34291c' },
252
+ base: { backgroundColor: '#fbf1c7', textColor: '#34291c' },
253
253
  soft: { backgroundColor: '#eadbb8', textColor: '#34291c' },
254
254
  emphasis: { backgroundColor: '#d8c18c', textColor: '#34291c' },
255
255
  },
256
- frame: { backgroundColor: '#f7efd9', textColor: '#34291c' },
256
+ frame: { backgroundColor: '#fbf1c7', textColor: '#34291c' },
257
257
  css: {
258
258
  surface: '#efe2c4',
259
259
  muted: '#6c5e4c',
@@ -287,13 +287,13 @@ const presentationPalettes = Object.freeze({
287
287
  modes: Object.freeze({
288
288
  light: createPaletteMode({
289
289
  appearance: 'light',
290
- page: { backgroundColor: '#f7efed', textColor: '#3c2929' },
290
+ page: { backgroundColor: '#f8e9e7', textColor: '#3c2929' },
291
291
  surfaces: {
292
- base: { backgroundColor: '#f7efed', textColor: '#3c2929' },
292
+ base: { backgroundColor: '#f8e9e7', textColor: '#3c2929' },
293
293
  soft: { backgroundColor: '#ecdeda', textColor: '#3c2929' },
294
294
  emphasis: { backgroundColor: '#ddc7c1', textColor: '#3c2929' },
295
295
  },
296
- frame: { backgroundColor: '#f7efed', textColor: '#3c2929' },
296
+ frame: { backgroundColor: '#f8e9e7', textColor: '#3c2929' },
297
297
  css: {
298
298
  surface: '#f0e3e0',
299
299
  muted: '#6d5558',
@@ -327,13 +327,13 @@ const presentationPalettes = Object.freeze({
327
327
  modes: Object.freeze({
328
328
  light: createPaletteMode({
329
329
  appearance: 'light',
330
- page: { backgroundColor: '#f1f3e8', textColor: '#263225' },
330
+ page: { backgroundColor: '#eaf0da', textColor: '#263225' },
331
331
  surfaces: {
332
- base: { backgroundColor: '#f1f3e8', textColor: '#263225' },
332
+ base: { backgroundColor: '#eaf0da', textColor: '#263225' },
333
333
  soft: { backgroundColor: '#e1e6d2', textColor: '#263225' },
334
334
  emphasis: { backgroundColor: '#cbd5b7', textColor: '#263225' },
335
335
  },
336
- frame: { backgroundColor: '#f1f3e8', textColor: '#263225' },
336
+ frame: { backgroundColor: '#eaf0da', textColor: '#263225' },
337
337
  css: {
338
338
  surface: '#e7eadb',
339
339
  muted: '#52604c',
@@ -367,16 +367,16 @@ const presentationPalettes = Object.freeze({
367
367
  modes: Object.freeze({
368
368
  light: createPaletteMode({
369
369
  appearance: 'light',
370
- page: { backgroundColor: '#f2f4fb', textColor: '#25243a' },
370
+ page: { backgroundColor: '#e8ebfa', textColor: '#25243a' },
371
371
  surfaces: {
372
- base: { backgroundColor: '#f2f4fb', textColor: '#25243a' },
372
+ base: { backgroundColor: '#e8ebfa', textColor: '#25243a' },
373
373
  soft: { backgroundColor: '#e2e6f3', textColor: '#25243a' },
374
374
  emphasis: { backgroundColor: '#d0d7eb', textColor: '#25243a' },
375
375
  },
376
- frame: { backgroundColor: '#f2f4fb', textColor: '#25243a' },
376
+ frame: { backgroundColor: '#e8ebfa', textColor: '#25243a' },
377
377
  css: {
378
378
  surface: '#e8eaf5',
379
- muted: '#62647a',
379
+ muted: '#5b5d73',
380
380
  soft: '#d5daec',
381
381
  accent: '#3b568f',
382
382
  border: 'rgb(37 36 58 / 16%)',
@@ -467,7 +467,6 @@ export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
467
467
  const paletteName = normalizedTheme.palette ?? 'near-monochrome';
468
468
  const palette = getPresentationPalette(paletteName);
469
469
  const appearance = normalizedTheme.appearance ?? {};
470
- const readerControls = normalizedTheme.readerControls ?? {};
471
470
  const defaultAppearance = appearance.default ?? 'system';
472
471
  if (!appearanceNames.includes(defaultAppearance)) {
473
472
  throw new Error(`appearance.default must be one of ${appearanceNames.join(', ')} in ${sourceLabel}.`);
@@ -488,9 +487,9 @@ export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
488
487
  },
489
488
  readerPreferences: {
490
489
  controls: {
491
- appearance: readerControls.appearance === true,
490
+ appearance: true,
492
491
  readingWidth: true,
493
- focusReading: readerControls.focusReading === true,
492
+ focusReading: false,
494
493
  },
495
494
  defaults: {
496
495
  appearance: defaultAppearance,
@@ -14,6 +14,7 @@ import {
14
14
  siteThemeLabel,
15
15
  } from './site-paths.mjs';
16
16
  import { localEditorNames, normalizeEditLinkBaseUrl } from './edit-source-link.mjs';
17
+ import { resolveLocale } from './locale-registry.mjs';
17
18
  import { readThemeConfig } from './theme-config.mjs';
18
19
  import { resolveThemeConfig } from './theme-presets.mjs';
19
20
  import { parseYamlConfig } from './yaml-config.mjs';
@@ -197,178 +198,6 @@ const readSiteUrl = (config) => {
197
198
  return url;
198
199
  };
199
200
 
200
- const localeLabels = Object.freeze({
201
- en: Object.freeze({
202
- breadcrumb: 'Breadcrumb',
203
- appearance: 'Appearance',
204
- appearanceDark: 'Dark',
205
- appearanceLight: 'Light',
206
- appearanceSystem: 'System',
207
- codeCopied: 'Copied',
208
- codeCopyFailed: 'Could not copy code',
209
- copyCode: 'Copy code',
210
- calloutCaution: 'Caution',
211
- calloutDanger: 'Danger',
212
- calloutImportant: 'Important',
213
- calloutNote: 'Note',
214
- calloutTip: 'Tip',
215
- calloutWarning: 'Warning',
216
- closeNavigation: 'Close navigation',
217
- editSource: 'Edit this page',
218
- displaySettings: 'Display',
219
- focusReading: 'Focus reading',
220
- footnoteBackReference: 'Back to reference {reference}',
221
- footnotes: 'Footnotes',
222
- readingWidth: 'Reading width',
223
- readingWidthNarrow: 'Narrow',
224
- readingWidthStandard: 'Standard',
225
- readingWidthWide: 'Wide',
226
- resetDisplaySettings: 'Reset',
227
- dismissBanner: 'Dismiss notice',
228
- built: 'Built',
229
- images: 'Images',
230
- imageCarousel: 'image carousel',
231
- imageInspection: 'Image inspection',
232
- imageInspectionActualSize: 'Show actual size',
233
- imageInspectionClose: 'Close image inspection',
234
- imageInspectionFit: 'Fit image to window',
235
- inspectImage: 'Inspect image: {description}',
236
- navigationCollapsedAll: 'All navigation items collapsed.',
237
- navigationCollapseAll: 'Collapse all',
238
- navigationControls: 'Navigation tree controls',
239
- navigationChildren: 'Child pages',
240
- navigationExpandedAll: 'All navigation items expanded.',
241
- navigationExpandAll: 'Expand all',
242
- navigationFilter: 'Filter pages and groups',
243
- navigationFilterEmpty: 'No matching pages or groups. The current page remains available.',
244
- navigationFilterMatches: 'Matching navigation items: {count}',
245
- navigationLocatedCurrent: 'Current page located.',
246
- navigationLocateCurrent: 'Locate current page',
247
- navigationMenu: 'Menu',
248
- nextImage: 'Next image',
249
- nextPage: 'Next page',
250
- note: 'Note',
251
- openInVsCode: 'Open in VS Code',
252
- notFound: 'Page not found',
253
- notFoundText: 'The requested page does not exist or may have moved.',
254
- pageMoved: 'Page moved',
255
- pageMovedText: 'This address now identifies',
256
- pageNavigation: 'Page contents',
257
- pageSequence: 'Page sequence',
258
- pageSections: 'Sections',
259
- previousImage: 'Previous image',
260
- previousPage: 'Previous page',
261
- returnHome: 'Go to the homepage',
262
- search: 'Search',
263
- searchDescription: 'Search the published content on this site.',
264
- searchLoading: 'Loading search…',
265
- searchNoScript: 'Search requires JavaScript. Use the page navigation when JavaScript is unavailable.',
266
- searchUnavailable: 'Search is unavailable. During local work, run norna build:local to create or refresh the search index.',
267
- siteBanners: 'Site notices',
268
- siteNavigation: 'Pages',
269
- skipToContent: 'Skip to content',
270
- tableColumns: 'Table columns',
271
- tableNextColumns: 'Show next columns',
272
- tableOverflowDescription: 'More table columns are available horizontally.',
273
- tablePreviousColumns: 'Show previous columns',
274
- }),
275
- sv: Object.freeze({
276
- breadcrumb: 'Brödsmulor',
277
- appearance: 'Utseende',
278
- appearanceDark: 'Mörkt',
279
- appearanceLight: 'Ljust',
280
- appearanceSystem: 'System',
281
- codeCopied: 'Kopierat',
282
- codeCopyFailed: 'Kunde inte kopiera koden',
283
- copyCode: 'Kopiera kod',
284
- calloutCaution: 'Var försiktig',
285
- calloutDanger: 'Fara',
286
- calloutImportant: 'Viktigt',
287
- calloutNote: 'Notera',
288
- calloutTip: 'Tips',
289
- calloutWarning: 'Varning',
290
- closeNavigation: 'Stäng navigationen',
291
- editSource: 'Redigera den här sidan',
292
- displaySettings: 'Visning',
293
- focusReading: 'Fokuserad läsning',
294
- footnoteBackReference: 'Tillbaka till referens {reference}',
295
- footnotes: 'Fotnoter',
296
- readingWidth: 'Textbredd',
297
- readingWidthNarrow: 'Smal',
298
- readingWidthStandard: 'Standard',
299
- readingWidthWide: 'Bred',
300
- resetDisplaySettings: 'Återställ',
301
- dismissBanner: 'Stäng meddelande',
302
- built: 'Byggd',
303
- images: 'Bilder',
304
- imageCarousel: 'bildkarusell',
305
- imageInspection: 'Bildgranskning',
306
- imageInspectionActualSize: 'Visa faktisk storlek',
307
- imageInspectionClose: 'Stäng bildgranskning',
308
- imageInspectionFit: 'Anpassa bilden till fönstret',
309
- inspectImage: 'Granska bild: {description}',
310
- navigationCollapsedAll: 'Alla navigationsposter har fällts ihop.',
311
- navigationCollapseAll: 'Fäll ihop alla',
312
- navigationControls: 'Kontroller för navigationsträdet',
313
- navigationChildren: 'Undersidor',
314
- navigationExpandedAll: 'Alla navigationsposter har fällts ut.',
315
- navigationExpandAll: 'Fäll ut alla',
316
- navigationFilter: 'Filtrera sidor och grupper',
317
- navigationFilterEmpty: 'Inga sidor eller grupper matchar. Den aktuella sidan är fortfarande tillgänglig.',
318
- navigationFilterMatches: 'Matchande navigationsposter: {count}',
319
- navigationLocatedCurrent: 'Den aktuella sidan har hittats.',
320
- navigationLocateCurrent: 'Hitta aktuell sida',
321
- navigationMenu: 'Meny',
322
- nextImage: 'Nästa bild',
323
- nextPage: 'Nästa sida',
324
- note: 'Not',
325
- openInVsCode: 'Öppna i VS Code',
326
- notFound: 'Sidan hittades inte',
327
- notFoundText: 'Den begärda sidan finns inte eller kan ha flyttats.',
328
- pageMoved: 'Sidan har flyttats',
329
- pageMovedText: 'Den här adressen identifierar nu',
330
- pageNavigation: 'Sidinnehåll',
331
- pageSequence: 'Sidföljd',
332
- pageSections: 'Avsnitt',
333
- previousImage: 'Föregående bild',
334
- previousPage: 'Föregående sida',
335
- returnHome: 'Gå till startsidan',
336
- search: 'Sök',
337
- searchDescription: 'Sök i det publicerade innehållet på webbplatsen.',
338
- searchLoading: 'Laddar sökning…',
339
- searchNoScript: 'Sökning kräver JavaScript. Använd sidnavigeringen när JavaScript inte är tillgängligt.',
340
- searchUnavailable: 'Sökningen är inte tillgänglig. Kör norna build:local under lokalt arbete för att skapa eller uppdatera sökindexet.',
341
- siteBanners: 'Meddelanden',
342
- siteNavigation: 'Sidor',
343
- skipToContent: 'Hoppa till innehållet',
344
- tableColumns: 'Tabellkolumner',
345
- tableNextColumns: 'Visa nästa kolumner',
346
- tableOverflowDescription: 'Fler tabellkolumner är tillgängliga i sidled.',
347
- tablePreviousColumns: 'Visa föregående kolumner',
348
- }),
349
- });
350
-
351
- const readLocale = (config) => {
352
- const lang = config.language ?? 'en';
353
-
354
- if (typeof lang !== 'string' || !/^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]+)*$/.test(lang.trim())) {
355
- throw new Error(`language must be a valid language tag such as "en" or "sv" in ${siteConfigLabel}.`);
356
- }
357
-
358
- const normalizedLang = lang.trim();
359
- const languageKey = normalizedLang.toLowerCase().split('-')[0];
360
- const labels = localeLabels[languageKey];
361
-
362
- if (!labels) {
363
- throw new Error(`language "${normalizedLang}" has no built-in Norna UI text. Supported languages: ${Object.keys(localeLabels).join(', ')}.`);
364
- }
365
-
366
- return Object.freeze({
367
- lang: normalizedLang,
368
- labels,
369
- });
370
- };
371
-
372
201
  const rawConfig = assertObject(siteConfig, 'config YAML');
373
202
  const rawTheme = assertObject(themeConfig, 'theme YAML', siteThemeLabel);
374
203
  const siteUrl = readSiteUrl(rawConfig);
@@ -609,7 +438,7 @@ export const projectConfig = Object.freeze({
609
438
  'instant',
610
439
  ),
611
440
  }),
612
- locale: readLocale(rawConfig),
441
+ locale: resolveLocale(rawConfig.language, siteConfigLabel),
613
442
  });
614
443
 
615
444
  export default projectConfig;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'astro/zod';
2
2
  import { localEditorNames } from './edit-source-link.mjs';
3
3
  import { imagePresentationNames } from './image-presentation.mjs';
4
+ import { supportedLocaleTagPattern } from './locale-registry.mjs';
4
5
  import { navigationModeNames } from './navigation-model.mjs';
5
6
  import { presentationPaletteNames } from './presentation-palette-metadata.mjs';
6
7
  import { themePresetNames } from './theme-presets.mjs';
@@ -25,13 +26,6 @@ const themeAppearance = z.object({
25
26
  (value) => value.default !== undefined,
26
27
  'Specify default.',
27
28
  ).describe('Site-wide initial appearance. The default is system.');
28
- const readerControls = z.object({
29
- appearance: z.boolean().optional().describe('Show an Appearance control that lets readers choose System, Light, or Dark.'),
30
- focusReading: z.boolean().optional().describe('Show a Focus reading control that lets readers hide navigation, breadcrumbs, and the footer. Sites with tree navigation always show this control.'),
31
- }).strict().refine(
32
- (value) => value.appearance !== undefined || value.focusReading !== undefined,
33
- 'Specify appearance, focusReading, or both.',
34
- ).describe('Optional controls for the site-wide Display panel.');
35
29
  const spacingDensity = z.enum(['compact', 'normal', 'airy']).describe('Coordinated spacing density. Omit this to use the selected preset.');
36
30
  const contentSpacing = z.enum(['compact', 'normal', 'spacious']).describe('Vertical spacing between page sections and structured content blocks.');
37
31
  const backgroundPattern = z.enum(['uniform', 'alternating', 'accented']).describe('How coordinated backgrounds are assigned to H2 sections. Non-uniform patterns are unavailable with tree navigation.');
@@ -196,7 +190,7 @@ const themeBlocks = z.object({
196
190
  'Specify cardList.',
197
191
  ).describe('Optional site-wide defaults for structured Norna content blocks.');
198
192
  const configNavigation = z.object({
199
- mode: navigationMode.optional().describe('Navigation model. Omit this to let Norna choose from the site structure.'),
193
+ mode: navigationMode.optional().describe('How Norna presents the discovered page and heading hierarchy. Omit this to select automatic.'),
200
194
  }).strict().describe('Site-wide navigation behavior.');
201
195
  const themeSections = z.object({
202
196
  backgroundPattern: backgroundPattern.optional().describe('Section background pattern. Alternating and accented create full-width bands with sections or top navigation; tree navigation requires uniform.'),
@@ -244,7 +238,7 @@ const sitewideFooter = z.object({
244
238
 
245
239
  const configShape = {
246
240
  url: z.string().url().describe('Absolute public URL for the built site.'),
247
- language: z.string().regex(/^(?:en|sv)(?:-[a-zA-Z0-9]+)*$/).optional().describe('Site language tag using Norna\'s English or Swedish interface text; the default is en.'),
241
+ language: z.string().regex(supportedLocaleTagPattern).optional().describe('Site language tag selecting one complete built-in Norna interface language; the default is en.'),
248
242
  editLink: editLink.optional(),
249
243
  navigation: configNavigation.optional(),
250
244
  search: z.boolean().optional().default(false).describe('Generate a static site search page and Pagefind index. The default is false.'),
@@ -259,7 +253,6 @@ const siteShape = {
259
253
  const themeVisualShape = {
260
254
  preset: themePreset.optional().describe('Complete visual starting point. Add only the overrides the site actually needs.'),
261
255
  appearance: themeAppearance.optional(),
262
- readerControls: readerControls.optional(),
263
256
  corners: cornerTreatment.optional().describe('Site-wide corner treatment. Omit this to use the selected preset.'),
264
257
  layout: themeLayout.optional(),
265
258
  images: themeImages.optional(),
@@ -1,4 +1,8 @@
1
1
  import { documentationLink } from './documentation-links.mjs';
2
+ import {
3
+ localeDefinitions,
4
+ supportedQualifiedLocaleTagPatternSource,
5
+ } from './locale-registry.mjs';
2
6
  import { presentationPaletteNames } from './presentation-palette-metadata.mjs';
3
7
  import { getSchemaValueDefinition } from './schema-value-definitions.mjs';
4
8
 
@@ -105,20 +109,21 @@ const addValueDescriptions = (schema) => {
105
109
  const addLanguageSuggestions = (jsonSchema) => {
106
110
  const language = jsonSchema.properties?.language;
107
111
  if (!language) throw new Error('Generated config schema has no language property.');
108
- const definition = getSchemaValueDefinition(['en', 'sv']);
109
112
  language.default = 'en';
110
- language.examples = ['en', 'sv', 'en-GB', 'sv-SE'];
113
+ language.examples = ['en', 'sv', 'es', 'el', 'uk', 'sr-Cyrl', 'en-GB', 'pt-BR'];
111
114
  language.oneOf = [
112
- ...definition.values.map((value) => ({
113
- const: value,
114
- title: definition.options[value].title,
115
- description: definition.options[value].description,
115
+ ...localeDefinitions.map(({ tag, name, nativeName, script, requiresScript }) => ({
116
+ const: tag,
117
+ title: nativeName === name ? name : `${nativeName} — ${name}`,
118
+ description: requiresScript
119
+ ? `Use Norna's built-in ${name} interface text in the ${script} script.`
120
+ : `Use Norna's built-in ${name} interface text.`,
116
121
  })),
117
122
  {
118
123
  type: 'string',
119
- pattern: '^(?:en|sv)-[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$',
120
- title: 'Regional language tag',
121
- description: 'Use an English or Swedish regional language tag such as en-GB or sv-SE.',
124
+ pattern: supportedQualifiedLocaleTagPatternSource,
125
+ title: 'Regional or script-qualified tag',
126
+ description: 'Add a region to a supported language, such as en-GB, pt-BR, or sr-Cyrl-RS. Norna preserves the complete tag in generated HTML.',
122
127
  },
123
128
  ];
124
129
  };
@@ -156,9 +161,9 @@ const addConfigHelp = (jsonSchema) => {
156
161
  ], ['https://example.com/', 'https://owner.github.io/repository-name/']);
157
162
  addHelp(jsonSchema, 'language', [
158
163
  yamlExample('language: en-GB'),
159
- 'Sets the page language and selects Norna\'s built-in English or Swedish interface text. The default is `en`.',
164
+ 'Sets the language of the complete site and selects the matching built-in Norna interface text. Regional tags are preserved. The default is `en`; editorial Markdown is not translated.',
160
165
  documentationLink('Language reference', 'configuration.md', 'language'),
161
- ], ['en', 'sv', 'en-GB', 'sv-SE']);
166
+ ], ['en', 'sv', 'es', 'el', 'uk', 'sr-Cyrl', 'en-GB', 'pt-BR']);
162
167
  addHelp(jsonSchema, 'editLink', [
163
168
  yamlExample('editLink:\n localEditor: vscode\n baseUrl: https://github.com/owner/repository/edit/main/'),
164
169
  'Links each rendered page to its `content.md` source. A loopback development preview uses the explicitly selected local editor; published output and LAN previews use the optional remote base URL. Specify either destination or both.',
@@ -188,19 +193,19 @@ const addConfigHelp = (jsonSchema) => {
188
193
  ], ['vscode']);
189
194
  addHelp(jsonSchema, 'navigation', [
190
195
  yamlExample('navigation:\n mode: automatic'),
191
- 'Sets the site-wide navigation policy. `automatic` uses sections for one page, top navigation for a flat multi-page site, and a stable left rail throughout a hierarchical site.',
192
- documentationLink('Navigation reference', 'pages.md', 'navigation'),
196
+ 'Chooses how Norna presents the discovered page and heading hierarchy. Keep `automatic` unless the site deliberately needs to retain one presentation as its structure changes.',
197
+ documentationLink('Navigation setting reference', 'configuration.md', 'navigation'),
193
198
  ]);
194
199
  addHelp(jsonSchema, 'search', [
195
200
  yamlExample('search: true'),
196
- 'Generates a localized `/search/` page and a static Pagefind index during each build. Search is off by default, and only the search page loads the search JavaScript.',
201
+ 'Generates a localized `/search/` page and a static Pagefind index during each build. Search is off by default. Only the search page loads the search engine and index; a small navigation script remembers where readers opened it.',
197
202
  documentationLink('Search reference', 'configuration.md', 'search'),
198
203
  ], [true, false]);
199
204
  addFieldHelp(
200
205
  jsonSchema,
201
206
  'navigation.mode',
202
207
  'navigation:\n mode: automatic',
203
- 'pages.md',
208
+ 'configuration.md',
204
209
  'navigation',
205
210
  ['automatic', 'sections', 'top', 'tree'],
206
211
  );
@@ -278,7 +283,7 @@ const addThemeHelp = (jsonSchema) => {
278
283
  ], presentationPaletteNames);
279
284
  addHelp(jsonSchema, 'appearance', [
280
285
  yamlExample('appearance:\n default: system'),
281
- 'Selects the initial Appearance. Omit this setting to follow the visitor\'s system preference. Enable the reader-facing choice under readerControls.',
286
+ 'Selects the initial Appearance. The Appearance choice is always available in the site-wide Display panel. Omit this setting to follow the visitor\'s system preference.',
282
287
  documentationLink('Appearance', 'theme.md', 'appearance'),
283
288
  ]);
284
289
  addFieldHelp(jsonSchema, 'appearance.default', 'appearance:\n default: system', 'theme.md', 'appearance', ['system', 'light', 'dark']);
@@ -289,32 +294,6 @@ const addThemeHelp = (jsonSchema) => {
289
294
  file: 'theme.md',
290
295
  anchor: 'appearance',
291
296
  })]);
292
- addHelp(jsonSchema, 'readerControls', [
293
- yamlExample('readerControls:\n appearance: true\n focusReading: true'),
294
- 'Choose which optional controls readers can use in the site-wide Display panel.',
295
- '`appearance: true` lets readers choose System, Light, or Dark. `focusReading: true` lets them hide navigation, breadcrumbs, and the footer while reading.',
296
- 'Reading width is always included. Sites with tree navigation always include Focus reading, even when it is not enabled here.',
297
- documentationLink('Reader Display controls', 'theme.md', 'reader-display-controls'),
298
- ]);
299
- for (const [propertyPath, example] of [
300
- ['readerControls.appearance', 'readerControls:\n appearance: true'],
301
- ['readerControls.focusReading', 'readerControls:\n focusReading: true'],
302
- ]) {
303
- addFieldHelp(
304
- jsonSchema,
305
- propertyPath,
306
- example,
307
- 'theme.md',
308
- 'reader-display-controls',
309
- );
310
- }
311
- addSnippets(jsonSchema, 'readerControls', [schemaSnippet({
312
- label: 'Configure the Display panel',
313
- body: 'readerControls:\n appearance: ${1:true}\n focusReading: ${2:true}',
314
- description: 'Show optional Appearance and Focus reading controls. Reading width is already included, and tree navigation always includes Focus reading.',
315
- file: 'theme.md',
316
- anchor: 'reader-display-controls',
317
- })]);
318
297
  addHelp(jsonSchema, 'sections', [
319
298
  yamlExample('sections:\n backgroundPattern: alternating'),
320
299
  'Chooses whether H2 section backgrounds stay uniform, alternate between two backgrounds or move from base to soft to emphasis and back through soft. Non-uniform patterns are available with sections and top navigation; tree navigation requires uniform.',
@@ -628,7 +607,7 @@ const addContentHelp = (jsonSchema) => {
628
607
  const addCategoryHelp = (jsonSchema) => {
629
608
  jsonSchema.markdownDescription = [
630
609
  yamlExample('label: Guides'),
631
- '`category.yaml` creates a navigation-only group for child pages. It has no page content and produces no URL of its own.',
610
+ '`category.yaml` groups child pages without editorial content. Its URL opens the first listed direct page, or a generated child list when the first child is a category.',
632
611
  documentationLink('Navigation category reference', 'pages.md', 'navigation-categories'),
633
612
  ].join('\n\n');
634
613
  addHelp(jsonSchema, 'label', [
@@ -23,14 +23,10 @@ export const schemaValueDefinitions = Object.freeze([
23
23
  smooth: option('Browser smooth', 'Use the browser\'s native smooth scrolling for anchor navigation.'),
24
24
  }),
25
25
  definition(['automatic', 'sections', 'top', 'tree'], {
26
- automatic: option('Automatic', 'Choose sections for one page, top navigation for a flat multi-page site, and a stable left rail throughout a site with listed child pages or categories.'),
27
- sections: option('Sections', 'Navigate the sections of the current page.'),
28
- top: option('Top', 'Use pages in the top navigation and sections below them.'),
29
- tree: option('Tree', 'Use a left page rail and a separate right H2/H3 contents rail where the page has enough headings.'),
30
- }),
31
- definition(['en', 'sv'], {
32
- en: option('English', 'Use Norna\'s built-in English interface text.'),
33
- sv: option('Swedish', 'Use Norna\'s built-in Swedish interface text.'),
26
+ automatic: option('Automatic', 'Choose one site-wide navigation presentation from the listed page hierarchy. This is the default.'),
27
+ sections: option('Sections', 'Keep a one-page site title and its H2 sections in sticky page navigation.'),
28
+ top: option('Top', 'Keep top-level pages in the global row and give the current page section navigation when needed.'),
29
+ tree: option('Tree', 'Keep top-level areas global and add a persistent left page tree. Deep branches can add a right H2/H3 contents rail.'),
34
30
  }),
35
31
  definition(['vscode'], {
36
32
  vscode: option('Visual Studio Code', 'Open the current page source in the locally installed Visual Studio Code application.'),
@@ -62,6 +62,7 @@ const formatDiagnostic = ({ code, fix, label, line, message, offset }) => ({
62
62
  line,
63
63
  message: `${label} line ${line}: ${message}`,
64
64
  offset,
65
+ severity: 'error',
65
66
  });
66
67
 
67
68
  export const getSemanticCalloutDiagnostics = (tree, options = {}) => {
@@ -101,7 +102,8 @@ export const getSemanticCalloutDiagnostics = (tree, options = {}) => {
101
102
  issue = {
102
103
  code: 'unknown-semantic-callout-type',
103
104
  message: `Unknown semantic callout type "${marker.rawType}".`,
104
- fix: `Use one of: ${semanticCalloutTypes.join(', ')}.`,
105
+ fix: `Use one of: ${semanticCalloutTypes.join(', ')}. The block will remain a neutral blockquote until then.`,
106
+ severity: 'warning',
105
107
  };
106
108
  } else if (marker.title) {
107
109
  issue = {
@@ -117,7 +119,7 @@ export const getSemanticCalloutDiagnostics = (tree, options = {}) => {
117
119
  };
118
120
  }
119
121
 
120
- if (issue) diagnostics.push(formatDiagnostic({ ...issue, label, line, offset }));
122
+ if (issue) diagnostics.push({ ...formatDiagnostic({ ...issue, label, line, offset }), severity: issue.severity ?? 'error' });
121
123
  }
122
124
 
123
125
  for (const child of node.children ?? []) visit(child, nextBlockquoteDepth);
@@ -67,7 +67,6 @@ const knownNestedFrontmatterKeys = new Set([
67
67
  'palette',
68
68
  'preset',
69
69
  'profile',
70
- 'readerControls',
71
70
  'readingWidth',
72
71
  'rhythm',
73
72
  'sectionGap',
@@ -171,7 +170,10 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
171
170
  fix = fileKind === 'page theme'
172
171
  ? 'Remove "colorMode:" from this page theme. Set "appearance:" in the root theme.yaml when an override is needed.'
173
172
  : 'Replace "colorMode:" with "appearance:".';
174
- } else if (fileKind === 'page theme' && ['preset', 'appearance', 'readerControls', 'corners', 'palette', 'typography'].includes(key)) {
173
+ } else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'readerControls') {
174
+ message = `Frontmatter line ${lineNumber}: Theme setting "readerControls" was removed. Appearance and reading width are always available; Focus reading follows tree navigation.`;
175
+ fix = 'Remove "readerControls:". Use "appearance.default" for the initial Appearance; reading width is always available and tree navigation provides Focus reading.';
176
+ } else if (fileKind === 'page theme' && ['preset', 'appearance', 'corners', 'palette', 'typography'].includes(key)) {
175
177
  message = `Frontmatter line ${lineNumber}: page themes may not define site-wide visual identity through "${key}".`;
176
178
  fix = `Move "${key}:" to the root theme.yaml. Page themes may set only layout.textWidth, layout.contentSpacing, images, and sections.backgroundPattern.`;
177
179
  } else if ((fileKind === 'theme' || fileKind === 'page theme') && ['logo', 'site'].includes(key)) {