@janga/norna 0.7.27 → 0.7.28

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 (109) hide show
  1. package/README.md +37 -28
  2. package/astro.config.mjs +19 -1
  3. package/bin/norna-cli.mjs +5 -3
  4. package/package.json +9 -5
  5. package/schemas/card-list.schema.json +1 -1
  6. package/schemas/config.schema.json +66 -66
  7. package/schemas/content-frontmatter.schema.json +16 -7
  8. package/schemas/manifest.json +49 -3
  9. package/schemas/page-theme.schema.json +1585 -41
  10. package/schemas/root-theme.schema.json +1767 -0
  11. package/schemas/sitewide-content.schema.json +18 -18
  12. package/schemas/theme.schema.json +243 -243
  13. package/scripts/add-site-node.mjs +13 -161
  14. package/scripts/build-site.mjs +11 -6
  15. package/scripts/check-config.mjs +7 -23
  16. package/scripts/dev-local.mjs +17 -10
  17. package/scripts/export-theme-preset.mjs +2 -2
  18. package/scripts/generate-images.mjs +7 -6
  19. package/scripts/generate-schemas.mjs +9 -6
  20. package/scripts/lib/category-destinations.mjs +13 -12
  21. package/scripts/lib/documentation-links.mjs +1 -1
  22. package/scripts/lib/documentation-routes.json +67 -60
  23. package/scripts/lib/docusaurus-migration-audit.mjs +9 -6
  24. package/scripts/lib/editor-file-policy.mjs +18 -0
  25. package/scripts/lib/editor-language-service.mjs +53 -55
  26. package/scripts/lib/editor-page-addresses.mjs +55 -0
  27. package/scripts/lib/editor-page-placement.mjs +172 -0
  28. package/scripts/lib/editor-site-editing-tree.mjs +146 -0
  29. package/scripts/lib/editor-site-files.mjs +153 -0
  30. package/scripts/lib/editor-site-links.mjs +79 -0
  31. package/scripts/lib/editor-site-tree.mjs +151 -0
  32. package/scripts/lib/editor-source-files.mjs +87 -0
  33. package/scripts/lib/editor-theme-help.mjs +43 -0
  34. package/scripts/lib/locale-registry.mjs +1 -1
  35. package/scripts/lib/locales/en.mjs +1 -1
  36. package/scripts/lib/navigation-model.mjs +2 -44
  37. package/scripts/lib/navigation-review.mjs +14 -50
  38. package/scripts/lib/norna-markdown-blocks.mjs +1 -1
  39. package/scripts/lib/page-aliases.mjs +3 -1
  40. package/scripts/lib/page-model.mjs +17 -20
  41. package/scripts/lib/page-move-apply.mjs +117 -0
  42. package/scripts/lib/page-move-plan.mjs +27 -59
  43. package/scripts/lib/presentation.mjs +3 -3
  44. package/scripts/lib/project-config.mjs +7 -8
  45. package/scripts/lib/schema-definitions.mjs +11 -38
  46. package/scripts/lib/schema-editor-metadata.mjs +12 -59
  47. package/scripts/lib/schema-value-definitions.mjs +1 -1
  48. package/scripts/lib/site-content.mjs +7 -24
  49. package/scripts/lib/site-conventions.mjs +19 -1
  50. package/scripts/lib/site-navigation-tree.mjs +2 -7
  51. package/scripts/lib/site-node-create.mjs +169 -0
  52. package/scripts/lib/site-paths.mjs +22 -10
  53. package/scripts/lib/site-structure.mjs +72 -82
  54. package/scripts/lib/site-upgrade.mjs +30 -0
  55. package/scripts/lib/source-files.mjs +34 -0
  56. package/scripts/lib/theme-config.mjs +26 -68
  57. package/scripts/lib/theme-packages.mjs +47 -0
  58. package/scripts/lib/theme-presets.mjs +20 -53
  59. package/scripts/lib/typography.mjs +2 -2
  60. package/scripts/lib/yaml-config.mjs +1 -1
  61. package/scripts/lib/yaml-source-edit.mjs +94 -0
  62. package/scripts/move-site-page.mjs +3 -124
  63. package/scripts/show-typography.mjs +32 -17
  64. package/scripts/sync-content-sections.mjs +9 -7
  65. package/scripts/sync-site-public.mjs +1 -1
  66. package/scripts/upgrade-site.mjs +17 -0
  67. package/src/components/AreaNavigationMenu.astro +24 -0
  68. package/src/components/AreaNavigationPanel.astro +55 -0
  69. package/src/components/AreaNavigationSwitcher.astro +30 -0
  70. package/src/components/ImageStackEnhancement.astro +0 -7
  71. package/src/components/NavigationContinuity.astro +26 -0
  72. package/src/components/NavigationPageTree.astro +13 -4
  73. package/src/components/NavigationState.astro +11 -0
  74. package/src/components/NavigationTreeControls.astro +3 -2
  75. package/src/components/SearchPage.astro +9 -1
  76. package/src/components/SectionNavigationScript.astro +106 -26
  77. package/src/components/SiteNavigation.astro +45 -20
  78. package/src/components/SitePage.astro +37 -61
  79. package/src/components/SiteTreeNavigation.astro +38 -18
  80. package/src/components/TopPageMenu.astro +4 -3
  81. package/src/components/TreeNavigationScript.astro +50 -57
  82. package/src/content.config.ts +10 -8
  83. package/src/layouts/BaseLayout.astro +15 -39
  84. package/src/lib/areaNavigation.ts +42 -0
  85. package/src/lib/navigationContinuity.ts +212 -0
  86. package/src/lib/navigationFollowing.ts +28 -18
  87. package/src/lib/navigationState.mjs +88 -0
  88. package/src/lib/navigationStateScript.mjs +76 -0
  89. package/src/lib/navigationVisibility.ts +9 -0
  90. package/src/lib/pageThemes.ts +5 -42
  91. package/src/lib/sectionContent.ts +3 -2
  92. package/src/lib/siteNavigation.ts +6 -0
  93. package/src/lib/sitePages.ts +9 -6
  94. package/src/pages/404.astro +1 -1
  95. package/src/pages/[...slug].astro +5 -15
  96. package/src/pages/index.astro +1 -3
  97. package/src/styles/area-navigation.css +399 -0
  98. package/src/styles/content.css +2 -29
  99. package/src/styles/navigation-continuity.css +163 -0
  100. package/src/styles/page-layout.css +57 -68
  101. package/src/styles/responsive.css +0 -4
  102. package/starters/basic/README.md +9 -8
  103. package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
  104. package/schemas/category.schema.json +0 -20
  105. package/src/components/PageContentsNavigation.astro +0 -20
  106. /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
  107. /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
  108. /package/starters/basic/site/{config.yaml → site-config/settings.yaml} +0 -0
  109. /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
@@ -0,0 +1,76 @@
1
+ import { createNavigationStateHelpers } from './navigationState.mjs';
2
+
3
+ // Serialized into a parser-time script. Like the helpers, this entry point must
4
+ // only reference its arguments and browser globals, never module-scope values.
5
+ function restoreEarlyNavigationState(createHelpers) {
6
+ // Restore branches as the parser adds them, before they can be painted.
7
+ // pagereveal may precede this tree and is unavailable in some browsers.
8
+ const script = document.currentScript;
9
+ const container = script?.parentElement;
10
+ if (!container?.hasAttribute('data-navigation-root')) return;
11
+ const scope = script.dataset.navigationStateScope;
12
+ const { storageKeys, initialState, arrivalPolicy, readEntryState, pageIsOpen, sectionsAreOpen } = createHelpers();
13
+ const initialized = new WeakSet();
14
+ const restore = (refresh = false) => {
15
+ try {
16
+ // Establish the scroll limit before restoring a position near its end.
17
+ if (scope === 'desktop' && container.parentElement) {
18
+ const flowTop = container.parentElement.getBoundingClientRect().top + window.scrollY;
19
+ const stickyTop = parseFloat(getComputedStyle(container).top) || 0;
20
+ container.style.setProperty('--navigation-scrollport-top', `${Math.ceil(Math.max(flowTop, stickyTop))}px`);
21
+ }
22
+ const { stateKey, sharedStateKey } = storageKeys(scope,
23
+ container.dataset.navigationRoot, container.dataset.navigationStateRoot);
24
+ const state = initialState(sessionStorage.getItem(stateKey), sessionStorage.getItem(sharedStateKey),
25
+ readEntryState(stateKey));
26
+ const policy = arrivalPolicy(state);
27
+ for (const branch of container.querySelectorAll('.navigation-page-disclosure[data-page-path]')) {
28
+ if (!refresh && initialized.has(branch)) continue;
29
+ branch.open = pageIsOpen(state.disclosure, branch.dataset, policy);
30
+ initialized.add(branch);
31
+ }
32
+ for (const branch of container.querySelectorAll('.navigation-page-sections-disclosure[data-page-path]')) {
33
+ if (!refresh && initialized.has(branch)) continue;
34
+ branch.open = sectionsAreOpen(state.disclosure, branch.dataset, policy);
35
+ initialized.add(branch);
36
+ }
37
+ let scrollContainer = container;
38
+ for (let candidate = container; candidate; candidate = candidate.parentElement) {
39
+ if (/auto|scroll/.test(getComputedStyle(candidate).overflowY)) {
40
+ scrollContainer = candidate;
41
+ break;
42
+ }
43
+ }
44
+ if (state.local.scrollTop > 0 || state.fromEntry) scrollContainer.scrollTop = Math.max(0, state.local.scrollTop);
45
+ if (scope === 'desktop' && !state.fromEntry && state.local.scrollTop <= 0) {
46
+ // A direct arrival in a short viewport can put the selected page
47
+ // below the fold. Reveal its title now, before heading following is
48
+ // paused. An already visible title or restored position stays put.
49
+ const node = container.querySelector('[data-navigation-current-page="true"]');
50
+ const link = node?.querySelector(':scope > .navigation-page-link, :scope > .navigation-page-open-link');
51
+ if (link && link.getBoundingClientRect().width > 0) {
52
+ const box = scrollContainer.getBoundingClientRect();
53
+ const controls = container.querySelector('[data-tree-controls]')?.getBoundingClientRect();
54
+ const top = Math.max(box.top, controls?.bottom ?? box.top) + 12;
55
+ const bottom = box.bottom - 16;
56
+ const row = link.getBoundingClientRect();
57
+ if (row.top < top) scrollContainer.scrollTop += row.top - top;
58
+ else if (row.bottom > bottom) scrollContainer.scrollTop += row.bottom - bottom;
59
+ }
60
+ }
61
+ } catch {
62
+ // Ordinary links and native details remain usable without storage.
63
+ }
64
+ };
65
+ restore();
66
+ const observer = new MutationObserver(() => restore());
67
+ observer.observe(container, { childList: true, subtree: true });
68
+ document.addEventListener('DOMContentLoaded', () => {
69
+ restore();
70
+ observer.disconnect();
71
+ }, { once: true });
72
+ // Reapply before a transition snapshot, including a cached history entry.
73
+ window.addEventListener('pagereveal', () => restore(true));
74
+ }
75
+
76
+ export const getNavigationStateScript = () => `(${restoreEarlyNavigationState.toString()})(${createNavigationStateHelpers.toString()});`;
@@ -0,0 +1,9 @@
1
+ export const isNavigationElementDisplayed = (element: HTMLElement) => {
2
+ if (!element.getClientRects().length || getComputedStyle(element).visibility === 'hidden') return false;
3
+ // Closed native details can retain layout rectangles for their hidden links.
4
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
5
+ if (parent instanceof HTMLDetailsElement && !parent.open
6
+ && !parent.querySelector(':scope > summary')?.contains(element)) return false;
7
+ }
8
+ return true;
9
+ };
@@ -1,44 +1,7 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import {
4
- validatePageThemeYamlStructure,
5
- } from '../../scripts/lib/site-content.mjs';
6
- import { pageThemeSchema } from '../../scripts/lib/schema-definitions.mjs';
7
- import { sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
8
- import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
9
- import { parseYamlConfig } from '../../scripts/lib/yaml-config.mjs';
10
- import { getPageDirectoryAncestors } from '../../scripts/lib/page-model.mjs';
1
+ import { selectPageTheme } from '../../scripts/lib/theme-packages.mjs';
2
+ import { siteDir } from '../../scripts/lib/site-paths.mjs';
11
3
 
12
- type PageTheme = {
13
- id: string;
14
- data: Record<string, unknown>;
15
- };
16
-
17
- export const getPageTheme = async (pageDirectory: string | null): Promise<PageTheme | null> => {
18
- if (!pageDirectory) return null;
19
-
20
- const pageAncestors = getPageDirectoryAncestors(pageDirectory);
21
- let inheritedData: Record<string, unknown> = {};
22
- const inheritedIds: string[] = [];
23
- for (const pageAncestor of pageAncestors) {
24
- const themeSegments = pageAncestor.split('/');
25
- const themePath = path.join(sitePagesDir, ...themeSegments, 'theme.yaml');
26
- const source = await readFile(themePath, 'utf8').catch((error) => {
27
- if (error?.code === 'ENOENT') return null;
28
- throw error;
29
- });
30
- if (!source) continue;
31
- const themeLabel = `${sitePagesLabel}/${themeSegments.join('/')}/theme.yaml`;
32
-
33
- const data = parseYamlConfig(source, themeLabel, {
34
- schema: pageThemeSchema,
35
- validateStructure: validatePageThemeYamlStructure,
36
- });
37
- inheritedData = mergePageThemeConfig(inheritedData, data);
38
- inheritedIds.push(`pages/${themeSegments.join('/')}/theme`);
39
- }
40
-
41
- return inheritedIds.length > 0
42
- ? { id: inheritedIds.join(' + '), data: inheritedData }
43
- : null;
4
+ export const getPageTheme = async (pageDirectory: string) => {
5
+ const { selected } = await selectPageTheme({ siteRoot: siteDir, pageDirectory });
6
+ return { id: selected.label, data: selected.config };
44
7
  };
@@ -1,3 +1,4 @@
1
+ import { getPageImageSourceKey } from '../../scripts/lib/site-conventions.mjs';
1
2
  import { markdownToHtml } from 'satteri';
2
3
  import { groupRenderedTabs } from '../../scripts/lib/content-tabs.mjs';
3
4
  import projectConfig from '../../scripts/lib/project-config.mjs';
@@ -111,7 +112,7 @@ const applyH3HeadingIds = (
111
112
  return result;
112
113
  };
113
114
 
114
- const getImageSourceKey = (page: SitePage, image: string) => `pages/${page.pageDirectory}/images/${image}`;
115
+ const getImageSourceKey = (page: SitePage, image: string) => getPageImageSourceKey(page.pageDirectory, image);
115
116
 
116
117
  const renderHeadingTitleHtml = async (headingSource: string) => {
117
118
  const headingMarkdown = headingSource
@@ -178,7 +179,7 @@ const resolveContentBlocks = async (
178
179
  }
179
180
  if (block.type === 'page-list') {
180
181
  if (childPages.length === 0) {
181
- throw new Error(`${page.contentLabel} line ${block.line}: page-list has no listed direct child pages to display. Navigation categories are not pages. Add a listed direct child page or remove the block.`);
182
+ throw new Error(`${page.contentLabel} line ${block.line}: page-list has no listed direct child pages to display. Add a listed direct child page or remove the block.`);
182
183
  }
183
184
  resolvedBlocks.push({
184
185
  type: 'page-list',
@@ -7,6 +7,12 @@ import {
7
7
  getSequentialPageNavigation as getSequentialPageNavigationShared,
8
8
  } from '../../scripts/lib/site-navigation-tree.mjs';
9
9
 
10
+ export type CategoryDestinationMap = ReadonlyMap<string, { target: SitePage | null }>;
11
+
12
+ export const getNavigationDestination = (node: SiteNode, categoryDestinations?: CategoryDestinationMap): SiteNode => (
13
+ categoryDestinations?.get(node.pathname)?.target ?? node
14
+ );
15
+
10
16
  export type SiteNavigationEntry = {
11
17
  node: SiteNode;
12
18
  headings: HeadingNavigation[];
@@ -3,10 +3,10 @@ import path from 'node:path';
3
3
  import { decodePageEntryId, parsePageDirectoryPath } from '../../scripts/lib/page-model.mjs';
4
4
  import { parsePageMarkdown } from '../../scripts/lib/page-markdown.mjs';
5
5
  import { readSiteFile } from '../../scripts/lib/site-content.mjs';
6
- import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
6
+ import { homePageDirectory, getPageSourceDirectory } from '../../scripts/lib/site-conventions.mjs';
7
7
  import { assertCategoryDestinationModel, createCategoryDestinationModel } from '../../scripts/lib/category-destinations.mjs';
8
8
  import { getSiteStructure } from '../../scripts/lib/site-structure.mjs';
9
- import { siteDirLabel, sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
9
+ import { siteDir, siteDirLabel } from '../../scripts/lib/site-paths.mjs';
10
10
 
11
11
  type SiteEntry = CollectionEntry<'site'>;
12
12
 
@@ -44,6 +44,7 @@ export type SitePage = SiteNodeBase & {
44
44
  export type SiteCategory = SiteNodeBase & {
45
45
  kind: 'category';
46
46
  categorySourceLabel: string;
47
+ description?: string;
47
48
  };
48
49
 
49
50
  export type SiteNode = SitePage | SiteCategory;
@@ -75,8 +76,9 @@ const compareNumberPaths = (left: number[], right: number[]) => {
75
76
 
76
77
  const readPageMarkdownDocument = async (entry: SiteEntry) => {
77
78
  const pageDirectory = getPageDirectory(entry);
78
- const contentLabel = `${sitePagesLabel}/${pageDirectory}/content.md`;
79
- const contentPath = path.join(sitePagesDir, pageDirectory, 'content.md');
79
+ const contentRelativePath = [getPageSourceDirectory(pageDirectory), 'content.md'].filter(Boolean).join('/');
80
+ const contentLabel = `${siteDirLabel}/${contentRelativePath}`;
81
+ const contentPath = path.join(siteDir, contentRelativePath);
80
82
  const { body, frontmatter } = await readSiteFile(contentPath, contentLabel);
81
83
  const markdownDocument = await parsePageMarkdown(body, {
82
84
  label: contentLabel,
@@ -141,6 +143,7 @@ const createSiteCategory = (category: Awaited<ReturnType<typeof getSiteStructure
141
143
  pathSegment: category.pagePath,
142
144
  pathname: getPagePathname(category.pagePath),
143
145
  categorySourceLabel: category.categorySourceLabel,
146
+ description: category.description,
144
147
  pageDirectory: category.pageDirectory,
145
148
  pageDirectories: category.pageDirectories,
146
149
  pageId: category.pageId,
@@ -161,7 +164,7 @@ const createSitePages = async (entries: SiteEntry[]) => {
161
164
 
162
165
  for (const page of pages) {
163
166
  if (page.isHome && page.navigation.listed === false) {
164
- throw new Error(`${homePageDirectory} is the required homepage and cannot set navigation.listed to false.`);
167
+ throw new Error(`The homepage at ${siteDirLabel}/root/content.md cannot set navigation.listed to false.`);
165
168
  }
166
169
  const existing = pathnames.get(page.pathname);
167
170
  if (existing) {
@@ -190,7 +193,7 @@ const createSitePages = async (entries: SiteEntry[]) => {
190
193
  siblingPageOrders.set(pageOrderKey, page);
191
194
  }
192
195
  if (!pages.some(({ isHome }) => isHome)) {
193
- throw new Error(`Homepage page entry ${homePageDirectory} is missing.`);
196
+ throw new Error(`Homepage content ${siteDirLabel}/root/content.md is missing.`);
194
197
  }
195
198
 
196
199
  return pages.sort((left, right) => (
@@ -20,7 +20,7 @@ const homePage = sitePages.find((page) => page.isHome);
20
20
  if (!homePage) throw new Error('The required homepage is missing from the site page model.');
21
21
 
22
22
  const [siteTheme] = await getCollection('theme');
23
- if (!siteTheme) throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
23
+ if (!siteTheme) throw new Error('site/root/tree-theme.yaml is required. Add the shared site theme before building the site.');
24
24
  const [sitewide] = await getCollection('sitewide');
25
25
  const sitewideData = sitewide?.data ?? {};
26
26
  const siteThemeData = siteTheme.data;
@@ -45,28 +45,18 @@ export async function getStaticPaths() {
45
45
  ? [{ params: { slug: 'search' }, props: { searchPage: true } }]
46
46
  : [];
47
47
 
48
- const categoryPaths = siteModel.categoryDestinations.destinations.map((destination) => ({
49
- params: { slug: destination.category.pathSegment },
50
- props: destination.kind === 'redirect' ? {
51
- alias: {
52
- targetPathname: destination.target.pathname,
53
- targetTitle: destination.target.title,
54
- categoryTitle: destination.category.title,
55
- },
56
- } : { categoryPath: destination.pathname },
57
- }));
58
- return [...pagePaths, ...aliasPaths, ...searchPath, ...categoryPaths];
48
+ return [...pagePaths, ...aliasPaths, ...searchPath];
59
49
  }
60
50
 
61
- const { alias, entry, searchPage, categoryPath } = Astro.props;
51
+ const { alias, entry, searchPage } = Astro.props;
62
52
 
63
- if (!searchPage && !alias && !categoryPath && (!entry || isHomePageEntry(entry))) {
53
+ if (!searchPage && !alias && (!entry || isHomePageEntry(entry))) {
64
54
  throw new Error('Page entry is missing.');
65
55
  }
66
56
 
67
57
  const currentPage = entry ? await getSitePage(entry) : null;
68
58
 
69
- if (!searchPage && !alias && !categoryPath && !currentPage?.pathSegment) {
59
+ if (!searchPage && !alias && !currentPage?.pathSegment) {
70
60
  throw new Error(`Page "${entry.id}" needs a page id.`);
71
61
  }
72
62
  ---
@@ -75,4 +65,4 @@ if (!searchPage && !alias && !categoryPath && !currentPage?.pathSegment) {
75
65
  ? <SearchPage />
76
66
  : alias
77
67
  ? <PageAliasRedirect targetPathname={alias.targetPathname} targetTitle={alias.targetTitle} categoryTitle={alias.categoryTitle} />
78
- : <SitePage entry={entry} categoryPath={categoryPath} />}
68
+ : <SitePage entry={entry} />}
@@ -2,10 +2,8 @@
2
2
  import { getCollection } from 'astro:content';
3
3
  import SitePage from '../components/SitePage.astro';
4
4
  import { isHomePageEntry } from '../lib/sitePages';
5
- import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
5
+ import { siteContentLabel } from '../../scripts/lib/site-paths.mjs';
6
6
 
7
- const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
8
- const siteContentLabel = `${siteDirectory || 'site'}/pages/${homePageDirectory}/content.md`;
9
7
  const entries = await getCollection('site');
10
8
  const site = entries.find(isHomePageEntry);
11
9