@janga/norna 0.7.22 → 0.7.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +14 -18
  2. package/astro.config.mjs +15 -0
  3. package/bin/norna-cli.mjs +8 -2
  4. package/package.json +28 -3
  5. package/schemas/category.schema.json +2 -2
  6. package/schemas/config.schema.json +52 -24
  7. package/schemas/content-frontmatter.schema.json +25 -5
  8. package/schemas/page-theme.schema.json +53 -25
  9. package/schemas/sitewide-content.schema.json +19 -18
  10. package/schemas/theme.schema.json +385 -252
  11. package/scripts/build-site.mjs +1 -0
  12. package/scripts/check-config.mjs +36 -5
  13. package/scripts/deploy-site.mjs +0 -1
  14. package/scripts/dev-local.mjs +175 -112
  15. package/scripts/generate-search-index.mjs +57 -0
  16. package/scripts/init-site.mjs +1 -0
  17. package/scripts/lib/code-fence-metadata.mjs +223 -0
  18. package/scripts/lib/content-sync-apply.mjs +34 -0
  19. package/scripts/lib/content-sync-plan.mjs +179 -0
  20. package/scripts/lib/edit-source-link.mjs +49 -0
  21. package/scripts/lib/editor-language-service.mjs +160 -12
  22. package/scripts/lib/heading-ids.mjs +1 -1
  23. package/scripts/lib/image-presentation.mjs +6 -0
  24. package/scripts/lib/markdown-links.mjs +182 -0
  25. package/scripts/lib/navigation-model.mjs +44 -12
  26. package/scripts/lib/navigation-review.mjs +396 -0
  27. package/scripts/lib/norna-markdown-blocks.mjs +74 -26
  28. package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
  29. package/scripts/lib/page-aliases.mjs +184 -0
  30. package/scripts/lib/page-markdown.mjs +88 -7
  31. package/scripts/lib/page-move-plan.mjs +659 -0
  32. package/scripts/lib/presentation-palette-metadata.mjs +1 -1
  33. package/scripts/lib/presentation.mjs +10 -37
  34. package/scripts/lib/project-config.mjs +146 -19
  35. package/scripts/lib/public-asset-conventions.mjs +36 -2
  36. package/scripts/lib/schema-definitions.mjs +43 -15
  37. package/scripts/lib/schema-editor-metadata.mjs +60 -32
  38. package/scripts/lib/schema-value-definitions.mjs +6 -2
  39. package/scripts/lib/semantic-callouts.mjs +128 -0
  40. package/scripts/lib/site-content.mjs +28 -5
  41. package/scripts/lib/site-link-graph.mjs +357 -0
  42. package/scripts/lib/site-navigation-tree.mjs +85 -0
  43. package/scripts/lib/site-page-urls.mjs +10 -0
  44. package/scripts/lib/sitemap.mjs +34 -0
  45. package/scripts/lib/social-image-assets.mjs +30 -0
  46. package/scripts/lib/theme-presets.mjs +53 -25
  47. package/scripts/lib/theme-profiles.mjs +6 -8
  48. package/scripts/lib/yaml-config.mjs +6 -2
  49. package/scripts/move-site-page.mjs +256 -0
  50. package/scripts/review-navigation.mjs +32 -0
  51. package/scripts/sync-content-sections.mjs +182 -216
  52. package/scripts/sync-site-public.mjs +43 -3
  53. package/src/components/CodeBlockCopyScript.astro +97 -0
  54. package/src/components/DisplaySettings.astro +4 -4
  55. package/src/components/EditSourceLink.astro +17 -0
  56. package/src/components/ImageCarousel.astro +6 -3
  57. package/src/components/NavigationPageTree.astro +81 -86
  58. package/src/components/NavigationTreeControls.astro +77 -0
  59. package/src/components/PageAliasRedirect.astro +49 -0
  60. package/src/components/PageContentsNavigation.astro +21 -0
  61. package/src/components/PageList.astro +33 -0
  62. package/src/components/PageSequenceNavigation.astro +37 -0
  63. package/src/components/SearchPage.astro +151 -0
  64. package/src/components/SectionNavigationScript.astro +62 -6
  65. package/src/components/SiteNavigation.astro +50 -11
  66. package/src/components/SitePage.astro +113 -20
  67. package/src/components/SiteSection.astro +16 -3
  68. package/src/components/SiteTreeNavigation.astro +8 -2
  69. package/src/components/TreeNavigationScript.astro +207 -26
  70. package/src/content.config.ts +5 -2
  71. package/src/layouts/BaseLayout.astro +71 -10
  72. package/src/lib/readerPreferencesScript.mjs +2 -2
  73. package/src/lib/sectionContent.ts +32 -3
  74. package/src/lib/siteNavigation.ts +31 -52
  75. package/src/lib/sitePages.ts +3 -2
  76. package/src/lib/sitePublicAssets.ts +1 -0
  77. package/src/pages/404.astro +110 -0
  78. package/src/pages/[...slug].astro +45 -8
  79. package/src/styles/content.css +718 -0
  80. package/src/styles/foundations.css +284 -0
  81. package/src/styles/global.css +6 -2510
  82. package/src/styles/media.css +518 -0
  83. package/src/styles/navigation.css +526 -0
  84. package/src/styles/page-layout.css +786 -0
  85. package/src/styles/responsive.css +594 -0
  86. package/starters/basic/README.md +6 -19
  87. package/starters/basic/package.json +1 -0
  88. package/starters/basic/site/pages/000-home/content.md +13 -50
@@ -20,9 +20,9 @@ interface Props {
20
20
  const { controls, defaults } = Astro.props;
21
21
  const labels = projectConfig.locale.labels;
22
22
  const appearanceOptions = [
23
- { value: 'system', label: labels.colorModeSystem },
24
- { value: 'light', label: labels.colorModeLight },
25
- { value: 'dark', label: labels.colorModeDark },
23
+ { value: 'system', label: labels.appearanceSystem },
24
+ { value: 'light', label: labels.appearanceLight },
25
+ { value: 'dark', label: labels.appearanceDark },
26
26
  ] as const;
27
27
  const widthOptions = [
28
28
  { value: 'narrow', label: labels.readingWidthNarrow },
@@ -51,7 +51,7 @@ const widthOptions = [
51
51
 
52
52
  {controls.appearance && (
53
53
  <fieldset>
54
- <legend>{labels.colorMode}</legend>
54
+ <legend>{labels.appearance}</legend>
55
55
  <div class="display-settings-segmented">
56
56
  {appearanceOptions.map((option) => (
57
57
  <label>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { PencilLine } from '@lucide/astro';
3
+
4
+ interface Props {
5
+ href: string;
6
+ label: string;
7
+ }
8
+
9
+ const { href, label } = Astro.props;
10
+ ---
11
+
12
+ <p class="edit-source-link" data-pagefind-ignore="all">
13
+ <a href={href}>
14
+ <PencilLine aria-hidden="true" />
15
+ <span>{label}</span>
16
+ </a>
17
+ </p>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { defaultImageMaxAvailableHeightPercent } from '../../scripts/lib/image-presentation.mjs';
3
4
  import { getImageAttributes } from '../lib/generatedImages';
4
5
  import {
5
6
  getCaptionId,
@@ -17,7 +18,7 @@ interface Props {
17
18
  desktop: number;
18
19
  mobile: number;
19
20
  };
20
- maxAvailableHeightPercent: {
21
+ maxAvailableHeightPercent?: {
21
22
  desktop: number;
22
23
  mobile: number;
23
24
  };
@@ -33,11 +34,13 @@ const {
33
34
  } = Astro.props;
34
35
  const managedImageSizes = getManagedImageSizes(maxAvailableWidthPercent);
35
36
  const carouselAspectRatio = getCarouselAspectRatio(images);
37
+ const carouselMaxAvailableHeightPercent = maxAvailableHeightPercent
38
+ ?? defaultImageMaxAvailableHeightPercent;
36
39
  const carouselStyle = carouselAspectRatio
37
40
  ? [
38
41
  `--image-carousel-aspect-ratio: ${carouselAspectRatio}`,
39
- `--image-carousel-width-from-height-desktop: ${carouselAspectRatio * maxAvailableHeightPercent.desktop}svh`,
40
- `--image-carousel-width-from-height-mobile: ${carouselAspectRatio * maxAvailableHeightPercent.mobile}svh`,
42
+ `--image-carousel-width-from-height-desktop: ${carouselAspectRatio * carouselMaxAvailableHeightPercent.desktop}svh`,
43
+ `--image-carousel-width-from-height-mobile: ${carouselAspectRatio * carouselMaxAvailableHeightPercent.mobile}svh`,
41
44
  ].join('; ')
42
45
  : undefined;
43
46
  ---
@@ -7,7 +7,7 @@ import PageContentsLinks from './PageContentsLinks.astro';
7
7
 
8
8
  interface Props {
9
9
  nodes: SiteNavigationNode[];
10
- currentPage: SitePage;
10
+ currentPage?: SitePage;
11
11
  variant: 'mobile' | 'sidebar' | 'submenu';
12
12
  integratePageSections?: boolean;
13
13
  }
@@ -15,21 +15,31 @@ interface Props {
15
15
  const { nodes, currentPage, variant, integratePageSections = false } = Astro.props;
16
16
  const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
17
17
  const hasBranch = (node: SiteNavigationNode) => node.children.length > 0;
18
- const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
19
- const isCurrentBranch = (node: SiteNode) => (
18
+ const isCurrentPage = (page: SitePage) => currentPage?.pathname === page.pathname;
19
+ const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
20
20
  currentPage.pageDirectory === node.pageDirectory
21
21
  || currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
22
- );
22
+ ));
23
23
  const getBranchId = (node: SiteNode) => [
24
24
  'navigation',
25
25
  variant,
26
26
  node.pagePath || 'home',
27
27
  'children',
28
28
  ].join('-').replace(/[^a-z0-9-]+/gi, '-');
29
+ const getSectionsId = (node: SiteNode) => [
30
+ 'navigation',
31
+ variant,
32
+ node.pagePath || 'home',
33
+ 'sections',
34
+ ].join('-').replace(/[^a-z0-9-]+/gi, '-');
29
35
  const hasPageSections = (node: SiteNavigationNode) => (
30
36
  node.node.kind === 'page' && node.headings.some((heading) => heading.depth === 2)
31
37
  );
32
- const hasPageDisclosure = (node: SiteNavigationNode) => hasBranch(node) || hasPageSections(node);
38
+ const showsPageSections = (node: SiteNavigationNode) => (
39
+ integratePageSections
40
+ && hasPageSections(node)
41
+ );
42
+ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' && hasBranch(node);
33
43
  ---
34
44
 
35
45
  <ul class:list={['navigation-page-tree', `navigation-page-tree-${variant}`]}>
@@ -37,9 +47,13 @@ const hasPageDisclosure = (node: SiteNavigationNode) => hasBranch(node) || hasPa
37
47
  <li
38
48
  class:list={['navigation-page-node', {
39
49
  'navigation-page-node-category': node.node.kind === 'category',
50
+ 'navigation-page-node-page-disclosure': node.node.kind === 'page' && usesPageDisclosure(node),
40
51
  'navigation-page-node-current': node.node.kind === 'page' && isCurrentPage(node.node),
41
52
  'navigation-page-node-current-branch': isCurrentBranch(node.node),
42
53
  }]}
54
+ data-navigation-title={node.node.title}
55
+ data-navigation-kind={node.node.kind}
56
+ data-navigation-current-page={node.node.kind === 'page' && isCurrentPage(node.node) ? 'true' : undefined}
43
57
  >
44
58
  {node.node.kind === 'category' ? (
45
59
  <details
@@ -69,103 +83,84 @@ const hasPageDisclosure = (node: SiteNavigationNode) => hasBranch(node) || hasPa
69
83
  />
70
84
  </div>
71
85
  </details>
72
- ) : variant !== 'submenu' && integratePageSections ? (
73
- hasPageDisclosure(node) ? (
74
- <details
75
- class:list={['navigation-page-branch', 'navigation-page-disclosure', `navigation-page-disclosure-${variant}`]}
76
- data-page-path={node.node.pagePath || '/'}
77
- data-current-branch={isCurrentBranch(node.node) ? 'true' : undefined}
78
- data-current-page={isCurrentPage(node.node) ? 'true' : undefined}
79
- data-has-sections={hasPageSections(node) ? 'true' : undefined}
80
- open={isCurrentBranch(node.node)}
81
- >
82
- <summary
83
- class="navigation-page-summary"
84
- aria-controls={getBranchId(node.node)}
85
- >
86
- <span class="navigation-page-summary-title">{node.node.title}</span>
87
- <span class="navigation-page-chevron" aria-hidden="true"></span>
88
- </summary>
89
- <a
90
- class:list={['navigation-page-open-link', { 'mobile-nav-destination': variant === 'mobile' }]}
91
- href={pageHref(node.node)}
92
- aria-current={isCurrentPage(node.node) ? 'page' : undefined}
93
- >
94
- {node.node.title}
95
- </a>
96
- <div id={getBranchId(node.node)} class="navigation-page-branch-content">
97
- {hasPageSections(node) && (
98
- <nav
99
- class="navigation-page-sections"
100
- aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
101
- >
102
- <PageContentsLinks
103
- headings={node.headings}
104
- variant={variant === 'mobile' ? 'mobile' : 'desktop'}
105
- pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
106
- />
107
- </nav>
108
- )}
109
- {hasBranch(node) && (
110
- <Astro.self
111
- nodes={node.children}
112
- currentPage={currentPage}
113
- variant={variant}
114
- integratePageSections={integratePageSections}
115
- />
116
- )}
117
- </div>
118
- </details>
119
- ) : (
120
- <a
121
- class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
122
- href={pageHref(node.node)}
123
- aria-current={isCurrentPage(node.node) ? 'page' : undefined}
124
- >
125
- {node.node.title}
126
- </a>
127
- )
128
- ) : variant !== 'submenu' ? (
129
- <>
130
- <a
131
- class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
132
- href={pageHref(node.node)}
133
- aria-current={isCurrentPage(node.node) ? 'page' : undefined}
86
+ ) : variant !== 'submenu' && usesPageDisclosure(node) ? (
87
+ <details
88
+ class:list={['navigation-page-branch', 'navigation-page-disclosure', `navigation-page-disclosure-${variant}`]}
89
+ data-page-path={node.node.pagePath || '/'}
90
+ data-current-branch={isCurrentBranch(node.node) ? 'true' : undefined}
91
+ data-current-page={isCurrentPage(node.node) ? 'true' : undefined}
92
+ open={isCurrentBranch(node.node)}
93
+ >
94
+ <summary
95
+ class="navigation-page-summary"
96
+ aria-controls={getBranchId(node.node)}
97
+ aria-label={`${projectConfig.locale.labels.navigationChildren}: ${node.node.title}`}
134
98
  >
135
- {node.node.title}
136
- </a>
137
- {variant === 'sidebar' && isCurrentPage(node.node) && hasPageSections(node) && (
99
+ <span class="navigation-page-chevron" aria-hidden="true"></span>
100
+ </summary>
101
+ </details>
102
+ <a
103
+ class:list={['navigation-page-open-link', { 'mobile-nav-destination': variant === 'mobile' }]}
104
+ href={pageHref(node.node)}
105
+ aria-current={isCurrentPage(node.node) ? 'page' : undefined}
106
+ >
107
+ {node.node.title}
108
+ </a>
109
+ <div id={getBranchId(node.node)} class="navigation-page-branch-content">
110
+ {showsPageSections(node) && (
138
111
  <nav
139
112
  class="navigation-page-sections"
140
113
  aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
141
114
  >
142
115
  <PageContentsLinks
143
116
  headings={node.headings}
144
- variant="desktop"
117
+ variant={variant === 'mobile' ? 'mobile' : 'desktop'}
118
+ pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
145
119
  />
146
120
  </nav>
147
121
  )}
148
122
  {hasBranch(node) && (
123
+ <Astro.self
124
+ nodes={node.children}
125
+ currentPage={currentPage}
126
+ variant={variant}
127
+ integratePageSections={integratePageSections}
128
+ />
129
+ )}
130
+ </div>
131
+ ) : variant !== 'submenu' ? (
132
+ <>
133
+ <a
134
+ class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
135
+ href={pageHref(node.node)}
136
+ aria-current={isCurrentPage(node.node) ? 'page' : undefined}
137
+ >
138
+ {node.node.title}
139
+ </a>
140
+ {showsPageSections(node) && (
149
141
  <details
150
- class="navigation-page-branch"
142
+ class="navigation-page-sections-disclosure"
151
143
  data-page-path={node.node.pagePath || '/'}
152
- data-current-branch={isCurrentBranch(node.node) ? 'true' : undefined}
153
- open={isCurrentBranch(node.node)}
144
+ data-current-page={isCurrentPage(node.node) ? 'true' : undefined}
145
+ open={isCurrentPage(node.node)}
154
146
  >
155
- <summary aria-controls={getBranchId(node.node)}>
156
- <span class="visually-hidden">
157
- {projectConfig.locale.labels.navigationChildren}: {node.node.title}
158
- </span>
159
- <span aria-hidden="true"></span>
147
+ <summary
148
+ aria-controls={getSectionsId(node.node)}
149
+ aria-label={`${projectConfig.locale.labels.pageSections}: ${node.node.title}`}
150
+ >
151
+ <span class="navigation-page-chevron" aria-hidden="true"></span>
160
152
  </summary>
161
- <div id={getBranchId(node.node)} class="navigation-page-branch-content">
162
- <Astro.self
163
- nodes={node.children}
164
- currentPage={currentPage}
165
- variant={variant}
166
- integratePageSections={integratePageSections}
153
+ <nav
154
+ id={getSectionsId(node.node)}
155
+ class="navigation-page-sections"
156
+ aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
157
+ >
158
+ <PageContentsLinks
159
+ headings={node.headings}
160
+ variant={variant === 'mobile' ? 'mobile' : 'desktop'}
161
+ pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
167
162
  />
168
- </div>
163
+ </nav>
169
164
  </details>
170
165
  )}
171
166
  </>
@@ -0,0 +1,77 @@
1
+ ---
2
+ import { ChevronsDownUp, ChevronsUpDown, ListFilter, LocateFixed } from '@lucide/astro';
3
+ import projectConfig from '../../scripts/lib/project-config.mjs';
4
+ import type { SiteNavigationNode } from '../lib/siteNavigation';
5
+
6
+ interface Props {
7
+ idPrefix: string;
8
+ nodes: SiteNavigationNode[];
9
+ }
10
+
11
+ const { idPrefix, nodes } = Astro.props;
12
+ const countNodes = (tree: SiteNavigationNode[]): number => tree.reduce(
13
+ (total, node) => total + 1 + countNodes(node.children),
14
+ 0,
15
+ );
16
+ const minimumUsefulNodeCount = 12;
17
+ const showControls = countNodes(nodes) >= minimumUsefulNodeCount;
18
+ const labels = projectConfig.locale.labels;
19
+ const filterId = `${idPrefix}-filter`;
20
+ const statusId = `${idPrefix}-status`;
21
+ ---
22
+
23
+ {showControls && (
24
+ <div
25
+ class="navigation-tree-controls"
26
+ data-tree-controls
27
+ data-tree-label-collapsed={labels.navigationCollapsedAll}
28
+ data-tree-label-expanded={labels.navigationExpandedAll}
29
+ data-tree-label-filter-matches={labels.navigationFilterMatches}
30
+ data-tree-label-located={labels.navigationLocatedCurrent}
31
+ hidden
32
+ >
33
+ <label class="navigation-tree-filter" for={filterId}>
34
+ <ListFilter aria-hidden="true" />
35
+ <span class="visually-hidden">{labels.navigationFilter}</span>
36
+ <input
37
+ id={filterId}
38
+ type="search"
39
+ autocomplete="off"
40
+ spellcheck="false"
41
+ placeholder={labels.navigationFilter}
42
+ aria-describedby={statusId}
43
+ data-tree-filter
44
+ />
45
+ </label>
46
+ <div class="navigation-tree-commands" role="group" aria-label={labels.navigationControls}>
47
+ <button
48
+ type="button"
49
+ aria-label={labels.navigationExpandAll}
50
+ title={labels.navigationExpandAll}
51
+ data-tree-expand-all
52
+ >
53
+ <ChevronsUpDown aria-hidden="true" />
54
+ </button>
55
+ <button
56
+ type="button"
57
+ aria-label={labels.navigationCollapseAll}
58
+ title={labels.navigationCollapseAll}
59
+ data-tree-collapse-all
60
+ >
61
+ <ChevronsDownUp aria-hidden="true" />
62
+ </button>
63
+ <button
64
+ type="button"
65
+ aria-label={labels.navigationLocateCurrent}
66
+ title={labels.navigationLocateCurrent}
67
+ data-tree-locate-current
68
+ >
69
+ <LocateFixed aria-hidden="true" />
70
+ </button>
71
+ </div>
72
+ <p class="navigation-tree-filter-empty" data-tree-filter-empty hidden>
73
+ {labels.navigationFilterEmpty}
74
+ </p>
75
+ <p id={statusId} class="visually-hidden" role="status" aria-live="polite" data-tree-status></p>
76
+ </div>
77
+ )}
@@ -0,0 +1,49 @@
1
+ ---
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { getAbsolutePageUrl } from '../../scripts/lib/site-page-urls.mjs';
4
+ import { withBasePath } from '../lib/basePath';
5
+
6
+ interface Props {
7
+ targetPathname: string;
8
+ targetTitle: string;
9
+ }
10
+
11
+ const { targetPathname, targetTitle } = Astro.props;
12
+ const { labels, lang } = projectConfig.locale;
13
+ const targetHref = withBasePath(projectConfig.site.basePath, targetPathname);
14
+ const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
15
+ ---
16
+
17
+ <!doctype html>
18
+ <html lang={lang} data-pagefind-ignore="all">
19
+ <head>
20
+ <meta charset="utf-8" />
21
+ <meta name="viewport" content="width=device-width" />
22
+ <meta name="robots" content="noindex, follow" />
23
+ <meta http-equiv="refresh" content={`0; url=${targetHref}`} />
24
+ <link rel="canonical" href={canonicalUrl} />
25
+ <title>{labels.pageMoved}: {targetTitle}</title>
26
+ <style is:inline>
27
+ :root {
28
+ color-scheme: light dark;
29
+ font-family: system-ui, sans-serif;
30
+ }
31
+
32
+ body {
33
+ margin: 0;
34
+ padding: 2rem;
35
+ }
36
+
37
+ main {
38
+ max-width: 42rem;
39
+ margin-inline: auto;
40
+ }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <main>
45
+ <h1>{labels.pageMoved}</h1>
46
+ <p>{labels.pageMovedText} <a href={targetHref}>{targetTitle}</a>.</p>
47
+ </main>
48
+ </body>
49
+ </html>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import type { HeadingNavigation } from '../lib/sectionContent';
4
+ import PageContentsLinks from './PageContentsLinks.astro';
5
+
6
+ interface Props {
7
+ headings: HeadingNavigation[];
8
+ pageTitle: string;
9
+ variant: 'inline' | 'rail';
10
+ }
11
+
12
+ const { headings, pageTitle, variant } = Astro.props;
13
+ const label = `${projectConfig.locale.labels.pageNavigation}: ${pageTitle}`;
14
+ ---
15
+
16
+ <aside class={`page-contents-navigation page-contents-navigation-${variant}`}>
17
+ <nav aria-label={label}>
18
+ <p class="page-contents-navigation-label">{projectConfig.locale.labels.pageNavigation}</p>
19
+ <PageContentsLinks headings={headings} variant="desktop" />
20
+ </nav>
21
+ </aside>
@@ -0,0 +1,33 @@
1
+ ---
2
+ import { ArrowRight } from '@lucide/astro';
3
+ import projectConfig from '../../scripts/lib/project-config.mjs';
4
+ import { withBasePath } from '../lib/basePath';
5
+
6
+ type PageListItem = {
7
+ title: string;
8
+ pathname: string;
9
+ description?: string;
10
+ };
11
+
12
+ interface Props {
13
+ pages: PageListItem[];
14
+ }
15
+
16
+ const { pages } = Astro.props;
17
+ ---
18
+
19
+ <nav class="child-page-list" aria-label={projectConfig.locale.labels.navigationChildren}>
20
+ <ul>
21
+ {pages.map((page) => (
22
+ <li>
23
+ <a href={withBasePath(projectConfig.site.basePath, page.pathname)}>
24
+ <span class="child-page-list-copy">
25
+ <strong>{page.title}</strong>
26
+ {page.description && <span>{page.description}</span>}
27
+ </span>
28
+ <ArrowRight aria-hidden="true" />
29
+ </a>
30
+ </li>
31
+ ))}
32
+ </ul>
33
+ </nav>
@@ -0,0 +1,37 @@
1
+ ---
2
+ import { ArrowLeft, ArrowRight } from '@lucide/astro';
3
+ import projectConfig from '../../scripts/lib/project-config.mjs';
4
+ import { withBasePath } from '../lib/basePath';
5
+ import type { SitePage } from '../lib/sitePages';
6
+
7
+ interface Props {
8
+ previous: SitePage | null;
9
+ next: SitePage | null;
10
+ }
11
+
12
+ const { previous, next } = Astro.props;
13
+ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
14
+ ---
15
+
16
+ {(previous || next) && (
17
+ <nav class="page-sequence-navigation" aria-label={projectConfig.locale.labels.pageSequence}>
18
+ {previous ? (
19
+ <a class="page-sequence-link page-sequence-previous" href={pageHref(previous)} rel="prev">
20
+ <ArrowLeft aria-hidden="true" />
21
+ <span>
22
+ <span class="page-sequence-direction">{projectConfig.locale.labels.previousPage}</span>
23
+ <span class="page-sequence-title">{previous.title}</span>
24
+ </span>
25
+ </a>
26
+ ) : <span class="page-sequence-empty" aria-hidden="true"></span>}
27
+ {next ? (
28
+ <a class="page-sequence-link page-sequence-next" href={pageHref(next)} rel="next">
29
+ <span>
30
+ <span class="page-sequence-direction">{projectConfig.locale.labels.nextPage}</span>
31
+ <span class="page-sequence-title">{next.title}</span>
32
+ </span>
33
+ <ArrowRight aria-hidden="true" />
34
+ </a>
35
+ ) : <span class="page-sequence-empty" aria-hidden="true"></span>}
36
+ </nav>
37
+ )}
@@ -0,0 +1,151 @@
1
+ ---
2
+ import { getCollection } from 'astro:content';
3
+ import { resolveNavigationModel } from '../../scripts/lib/navigation-model.mjs';
4
+ import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
5
+ import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
6
+ import SiteBanners from './SiteBanners.astro';
7
+ import SiteNavigation from './SiteNavigation.astro';
8
+ import SiteSection from './SiteSection.astro';
9
+ import BaseLayout from '../layouts/BaseLayout.astro';
10
+ import type { ResolvedSection } from '../lib/sectionContent';
11
+ import { flattenSiteNavigationTree, getListedSiteNavigationTree } from '../lib/siteNavigation';
12
+ import { getSiteModel, type SitePage } from '../lib/sitePages';
13
+ import { getLogoAsset } from '../lib/sitePublicAssets';
14
+ import { getToday, isVisibleToday } from '../lib/visibility';
15
+ import { withBasePath } from '../lib/basePath';
16
+
17
+ const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
18
+ const siteModel = await getSiteModel(await getCollection('site'));
19
+ const { nodes: siteNodes, pages: sitePages } = siteModel;
20
+ const homePage = sitePages.find((page) => page.isHome);
21
+ if (!homePage) throw new Error('The required homepage is missing from the site page model.');
22
+
23
+ const [siteTheme] = await getCollection('theme');
24
+ if (!siteTheme) throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
25
+ const [sitewide] = await getCollection('sitewide');
26
+ const sitewideData = sitewide?.data ?? {};
27
+ const siteThemeData = siteTheme.data;
28
+ const themeLabel = `${siteDirectory || 'site'}/${siteTheme.id}`;
29
+ const visualTheme = resolveThemeVisualConfig(siteThemeData, themeLabel);
30
+ const getPageContent = (page: SitePage) => page.markdownDocument.navigationHeadings.map((heading) => ({ ...heading }));
31
+ const siteNavigation = siteNodes.map((node) => {
32
+ const headings = node.kind === 'page' ? getPageContent(node) : [];
33
+ return {
34
+ node,
35
+ headings,
36
+ sections: headings
37
+ .filter((heading) => heading.depth === 2)
38
+ .map(({ id, title }) => ({ id, title })),
39
+ };
40
+ });
41
+ const renderedNavigationEntries = flattenSiteNavigationTree(getListedSiteNavigationTree(siteNavigation));
42
+ const navigationModel = resolveNavigationModel({
43
+ mode: projectConfig.navigation.mode,
44
+ nodes: renderedNavigationEntries.map(({ node, headings }) => ({
45
+ depth: Math.max(1, node.depth),
46
+ headings,
47
+ isHome: node.isHome,
48
+ kind: node.kind,
49
+ listed: node.navigation.listed,
50
+ pagePath: node.pagePath,
51
+ })),
52
+ });
53
+ const pagePresentation = resolvePagePresentation(siteThemeData, themeLabel, {
54
+ navigationMode: navigationModel.mode,
55
+ });
56
+ const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
57
+ const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: themeLabel });
58
+ const logoAsset = getLogoAsset();
59
+ const logo = logoAsset
60
+ ? {
61
+ src: logoAsset.href,
62
+ alt: homePage.title,
63
+ height: sitewideData.logo?.height,
64
+ }
65
+ : undefined;
66
+ const today = getToday();
67
+ const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
68
+ const labels = projectConfig.locale.labels;
69
+ const searchSection: ResolvedSection = {
70
+ id: null,
71
+ title: labels.search,
72
+ titleHtml: labels.search,
73
+ headingLevel: 1,
74
+ contentBlocks: [{
75
+ type: 'html',
76
+ html: `<p>${labels.searchDescription}</p><div class="norna-search" id="norna-search"><p data-search-status>${labels.searchLoading}</p><noscript><p>${labels.searchNoScript}</p></noscript></div>`,
77
+ }],
78
+ };
79
+ const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
80
+ || (
81
+ navigationModel.requestedMode === 'automatic'
82
+ && (navigationModel.hasNestedPages || navigationModel.hasCategories)
83
+ );
84
+ const bundlePath = withBasePath(projectConfig.site.basePath, '/pagefind/');
85
+ const searchScript = `(() => {
86
+ const root = document.querySelector('#norna-search');
87
+ const status = root?.querySelector('[data-search-status]');
88
+ if (!root) return;
89
+ const script = document.createElement('script');
90
+ script.src = ${JSON.stringify(`${bundlePath}pagefind-ui.js`)};
91
+ script.onload = () => {
92
+ status?.remove();
93
+ new window.PagefindUI({
94
+ element: '#norna-search',
95
+ bundlePath: ${JSON.stringify(bundlePath)},
96
+ baseUrl: ${JSON.stringify(projectConfig.site.basePath)},
97
+ showImages: false,
98
+ showSubResults: true
99
+ });
100
+ const labelInput = () => {
101
+ const input = root.querySelector('.pagefind-ui__search-input');
102
+ if (!input) return false;
103
+ input.setAttribute('aria-label', ${JSON.stringify(labels.search)});
104
+ return true;
105
+ };
106
+ if (!labelInput()) {
107
+ const observer = new MutationObserver(() => {
108
+ if (labelInput()) observer.disconnect();
109
+ });
110
+ observer.observe(root, { childList: true, subtree: true });
111
+ }
112
+ };
113
+ script.onerror = () => {
114
+ if (status) status.textContent = ${JSON.stringify(labels.searchUnavailable)};
115
+ };
116
+ document.head.append(script);
117
+ })();`;
118
+ ---
119
+
120
+ <BaseLayout
121
+ title={labels.search}
122
+ description={labels.searchDescription}
123
+ presentation={pagePresentation}
124
+ visualTheme={visualTheme}
125
+ frameColors={frameColors}
126
+ pathname="/search/"
127
+ footer={sitewideData.footer}
128
+ indexable={false}
129
+ >
130
+ <link slot="head" rel="stylesheet" href={`${bundlePath}pagefind-ui.css`} />
131
+ <SiteNavigation
132
+ logo={logo}
133
+ navigationEntries={siteNavigation}
134
+ navigationMode={navigationModel.mode}
135
+ showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
136
+ hasNavigationRailFrame={hasNavigationRailFrame}
137
+ frameColors={frameColors}
138
+ readerPreferences={pagePresentation.readerPreferences}
139
+ searchPage
140
+ />
141
+ {siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
142
+ <main class="site-content search-page-content" id="main-content" style={`--page-text-width: ${pageTextWidth}`}>
143
+ <SiteSection
144
+ section={searchSection}
145
+ pagePresentation={pagePresentation}
146
+ visualTheme={visualTheme}
147
+ sectionIndex={0}
148
+ />
149
+ </main>
150
+ <script is:inline set:html={searchScript} />
151
+ </BaseLayout>