@janga/norna 0.7.23 → 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.
- package/README.md +14 -18
- package/astro.config.mjs +12 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +13 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +45 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +12 -1
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +1 -0
- package/scripts/lib/code-fence-metadata.mjs +223 -0
- package/scripts/lib/edit-source-link.mjs +49 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +73 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +111 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +22 -5
- package/scripts/lib/schema-editor-metadata.mjs +19 -4
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CodeBlockCopyScript.astro +9 -4
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +5 -6
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SiteNavigation.astro +35 -8
- package/src/components/SitePage.astro +79 -26
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +8 -1
- package/src/components/TreeNavigationScript.astro +207 -26
- package/src/layouts/BaseLayout.astro +57 -2
- package/src/lib/sectionContent.ts +32 -3
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +242 -18
- package/src/styles/media.css +0 -5
- package/src/styles/navigation.css +31 -0
- package/src/styles/page-layout.css +438 -89
- package/src/styles/responsive.css +77 -8
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -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,
|
|
@@ -33,15 +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
|
-
|
|
40
|
-
|
|
41
|
-
`--image-carousel-width-from-height-desktop: ${carouselAspectRatio * maxAvailableHeightPercent.desktop}svh`,
|
|
42
|
-
`--image-carousel-width-from-height-mobile: ${carouselAspectRatio * maxAvailableHeightPercent.mobile}svh`,
|
|
43
|
-
]
|
|
44
|
-
: []),
|
|
42
|
+
`--image-carousel-width-from-height-desktop: ${carouselAspectRatio * carouselMaxAvailableHeightPercent.desktop}svh`,
|
|
43
|
+
`--image-carousel-width-from-height-mobile: ${carouselAspectRatio * carouselMaxAvailableHeightPercent.mobile}svh`,
|
|
45
44
|
].join('; ')
|
|
46
45
|
: undefined;
|
|
47
46
|
---
|
|
@@ -7,7 +7,7 @@ import PageContentsLinks from './PageContentsLinks.astro';
|
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
nodes: SiteNavigationNode[];
|
|
10
|
-
currentPage
|
|
10
|
+
currentPage?: SitePage;
|
|
11
11
|
variant: 'mobile' | 'sidebar' | 'submenu';
|
|
12
12
|
integratePageSections?: boolean;
|
|
13
13
|
}
|
|
@@ -15,26 +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) =>
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
? hasBranch(node)
|
|
36
|
-
: integratePageSections && hasPageDisclosure(node)
|
|
38
|
+
const showsPageSections = (node: SiteNavigationNode) => (
|
|
39
|
+
integratePageSections
|
|
40
|
+
&& hasPageSections(node)
|
|
37
41
|
);
|
|
42
|
+
const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' && hasBranch(node);
|
|
38
43
|
---
|
|
39
44
|
|
|
40
45
|
<ul class:list={['navigation-page-tree', `navigation-page-tree-${variant}`]}>
|
|
@@ -42,9 +47,13 @@ const usesPageDisclosure = (node: SiteNavigationNode) => (
|
|
|
42
47
|
<li
|
|
43
48
|
class:list={['navigation-page-node', {
|
|
44
49
|
'navigation-page-node-category': node.node.kind === 'category',
|
|
50
|
+
'navigation-page-node-page-disclosure': node.node.kind === 'page' && usesPageDisclosure(node),
|
|
45
51
|
'navigation-page-node-current': node.node.kind === 'page' && isCurrentPage(node.node),
|
|
46
52
|
'navigation-page-node-current-branch': isCurrentBranch(node.node),
|
|
47
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}
|
|
48
57
|
>
|
|
49
58
|
{node.node.kind === 'category' ? (
|
|
50
59
|
<details
|
|
@@ -80,26 +89,69 @@ const usesPageDisclosure = (node: SiteNavigationNode) => (
|
|
|
80
89
|
data-page-path={node.node.pagePath || '/'}
|
|
81
90
|
data-current-branch={isCurrentBranch(node.node) ? 'true' : undefined}
|
|
82
91
|
data-current-page={isCurrentPage(node.node) ? 'true' : undefined}
|
|
83
|
-
data-has-sections={hasPageSections(node) ? 'true' : undefined}
|
|
84
92
|
open={isCurrentBranch(node.node)}
|
|
85
93
|
>
|
|
86
94
|
<summary
|
|
87
95
|
class="navigation-page-summary"
|
|
88
96
|
aria-controls={getBranchId(node.node)}
|
|
97
|
+
aria-label={`${projectConfig.locale.labels.navigationChildren}: ${node.node.title}`}
|
|
89
98
|
>
|
|
90
|
-
<span class="navigation-page-summary-title">{node.node.title}</span>
|
|
91
99
|
<span class="navigation-page-chevron" aria-hidden="true"></span>
|
|
92
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) && (
|
|
111
|
+
<nav
|
|
112
|
+
class="navigation-page-sections"
|
|
113
|
+
aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
|
|
114
|
+
>
|
|
115
|
+
<PageContentsLinks
|
|
116
|
+
headings={node.headings}
|
|
117
|
+
variant={variant === 'mobile' ? 'mobile' : 'desktop'}
|
|
118
|
+
pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
|
|
119
|
+
/>
|
|
120
|
+
</nav>
|
|
121
|
+
)}
|
|
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
|
+
<>
|
|
93
133
|
<a
|
|
94
|
-
class:list={['navigation-page-
|
|
134
|
+
class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
|
|
95
135
|
href={pageHref(node.node)}
|
|
96
136
|
aria-current={isCurrentPage(node.node) ? 'page' : undefined}
|
|
97
137
|
>
|
|
98
138
|
{node.node.title}
|
|
99
139
|
</a>
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
{showsPageSections(node) && (
|
|
141
|
+
<details
|
|
142
|
+
class="navigation-page-sections-disclosure"
|
|
143
|
+
data-page-path={node.node.pagePath || '/'}
|
|
144
|
+
data-current-page={isCurrentPage(node.node) ? 'true' : undefined}
|
|
145
|
+
open={isCurrentPage(node.node)}
|
|
146
|
+
>
|
|
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>
|
|
152
|
+
</summary>
|
|
102
153
|
<nav
|
|
154
|
+
id={getSectionsId(node.node)}
|
|
103
155
|
class="navigation-page-sections"
|
|
104
156
|
aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
|
|
105
157
|
>
|
|
@@ -109,47 +161,6 @@ const usesPageDisclosure = (node: SiteNavigationNode) => (
|
|
|
109
161
|
pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
|
|
110
162
|
/>
|
|
111
163
|
</nav>
|
|
112
|
-
)}
|
|
113
|
-
{hasBranch(node) && (
|
|
114
|
-
<Astro.self
|
|
115
|
-
nodes={node.children}
|
|
116
|
-
currentPage={currentPage}
|
|
117
|
-
variant={variant}
|
|
118
|
-
integratePageSections={integratePageSections}
|
|
119
|
-
/>
|
|
120
|
-
)}
|
|
121
|
-
</div>
|
|
122
|
-
</details>
|
|
123
|
-
) : variant !== 'submenu' ? (
|
|
124
|
-
<>
|
|
125
|
-
<a
|
|
126
|
-
class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
|
|
127
|
-
href={pageHref(node.node)}
|
|
128
|
-
aria-current={isCurrentPage(node.node) ? 'page' : undefined}
|
|
129
|
-
>
|
|
130
|
-
{node.node.title}
|
|
131
|
-
</a>
|
|
132
|
-
{hasBranch(node) && (
|
|
133
|
-
<details
|
|
134
|
-
class="navigation-page-branch"
|
|
135
|
-
data-page-path={node.node.pagePath || '/'}
|
|
136
|
-
data-current-branch={isCurrentBranch(node.node) ? 'true' : undefined}
|
|
137
|
-
open={isCurrentBranch(node.node)}
|
|
138
|
-
>
|
|
139
|
-
<summary aria-controls={getBranchId(node.node)}>
|
|
140
|
-
<span class="visually-hidden">
|
|
141
|
-
{projectConfig.locale.labels.navigationChildren}: {node.node.title}
|
|
142
|
-
</span>
|
|
143
|
-
<span aria-hidden="true"></span>
|
|
144
|
-
</summary>
|
|
145
|
-
<div id={getBranchId(node.node)} class="navigation-page-branch-content">
|
|
146
|
-
<Astro.self
|
|
147
|
-
nodes={node.children}
|
|
148
|
-
currentPage={currentPage}
|
|
149
|
-
variant={variant}
|
|
150
|
-
integratePageSections={integratePageSections}
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
153
164
|
</details>
|
|
154
165
|
)}
|
|
155
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
|
+
)}
|
|
@@ -15,7 +15,7 @@ const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
<!doctype html>
|
|
18
|
-
<html lang={lang}>
|
|
18
|
+
<html lang={lang} data-pagefind-ignore="all">
|
|
19
19
|
<head>
|
|
20
20
|
<meta charset="utf-8" />
|
|
21
21
|
<meta name="viewport" content="width=device-width" />
|
|
@@ -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>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { Search } from '@lucide/astro';
|
|
2
3
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
4
|
import { withBasePath } from '../lib/basePath';
|
|
4
5
|
import type { SectionNavigation } from '../lib/sectionContent';
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
import type { SiteNode, SitePage } from '../lib/sitePages';
|
|
12
13
|
import DisplaySettings from './DisplaySettings.astro';
|
|
13
14
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
15
|
+
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
14
16
|
import SectionNavigationScript from './SectionNavigationScript.astro';
|
|
15
17
|
|
|
16
18
|
type FrameColors = {
|
|
@@ -24,7 +26,7 @@ interface Props {
|
|
|
24
26
|
alt: string;
|
|
25
27
|
height?: string;
|
|
26
28
|
};
|
|
27
|
-
currentPage
|
|
29
|
+
currentPage?: SitePage;
|
|
28
30
|
navigationEntries: SiteNavigationEntry[];
|
|
29
31
|
navigationMode: 'sections' | 'top' | 'tree';
|
|
30
32
|
showTopNavigationSubmenus: boolean;
|
|
@@ -42,6 +44,7 @@ interface Props {
|
|
|
42
44
|
focusReading: 'off' | 'on';
|
|
43
45
|
};
|
|
44
46
|
};
|
|
47
|
+
searchPage?: boolean;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const {
|
|
@@ -53,6 +56,7 @@ const {
|
|
|
53
56
|
hasNavigationRailFrame,
|
|
54
57
|
frameColors,
|
|
55
58
|
readerPreferences,
|
|
59
|
+
searchPage = false,
|
|
56
60
|
} = Astro.props;
|
|
57
61
|
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
58
62
|
? 'var(--color-page)'
|
|
@@ -67,10 +71,15 @@ const topLevelNavigation = siteNavigationTree;
|
|
|
67
71
|
const showSiteNavigation = topLevelNavigation.some(({ node }) => !node.isHome);
|
|
68
72
|
const homeNode = navigationEntries.find(({ node }) => node.isHome)?.node;
|
|
69
73
|
const homePage = homeNode?.kind === 'page' ? homeNode : currentPage;
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
if (!homePage) throw new Error('Site navigation requires a homepage.');
|
|
75
|
+
const currentPageNavigation = currentPage
|
|
76
|
+
? navigationEntries.find(({ node }) => node.kind === 'page' && node.pathname === currentPage.pathname)
|
|
77
|
+
: undefined;
|
|
78
|
+
const isCurrentPage = (page: SitePage) => currentPage?.pathname === page.pathname;
|
|
79
|
+
const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
|
|
80
|
+
currentPage.pageDirectory === node.pageDirectory
|
|
81
|
+
|| currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
|
|
82
|
+
));
|
|
74
83
|
const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
75
84
|
const getNavigationPage = (node: SiteNavigationNode) => getFirstPageInNavigationNode(node);
|
|
76
85
|
const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
|
|
@@ -94,7 +103,9 @@ const showPageNavigation = Boolean(
|
|
|
94
103
|
const showSinglePageTitleOnly = showSinglePageTopLink && currentPageSectionCount === 0;
|
|
95
104
|
const showMobileNavigation = showSiteNavigation || hasCurrentPageSectionMenu || hasTreePageContents;
|
|
96
105
|
const showReaderControls = Object.values(readerPreferences.controls).some(Boolean);
|
|
97
|
-
const
|
|
106
|
+
const showSearch = projectConfig.search.enabled;
|
|
107
|
+
const searchHref = withBasePath(projectConfig.site.basePath, '/search/');
|
|
108
|
+
const showDesktopNavigationRow = showSiteNavigation || Boolean(logo) || showReaderControls || showSearch;
|
|
98
109
|
const showNavigationRow = showMobileNavigation || showDesktopNavigationRow;
|
|
99
110
|
const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents || navigationMode === 'tree';
|
|
100
111
|
---
|
|
@@ -150,6 +161,18 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
150
161
|
</nav>
|
|
151
162
|
)}
|
|
152
163
|
|
|
164
|
+
{showSearch && (
|
|
165
|
+
<a
|
|
166
|
+
class="site-search-link"
|
|
167
|
+
href={searchHref}
|
|
168
|
+
aria-current={searchPage ? 'page' : undefined}
|
|
169
|
+
aria-label={projectConfig.locale.labels.search}
|
|
170
|
+
title={projectConfig.locale.labels.search}
|
|
171
|
+
>
|
|
172
|
+
<Search aria-hidden="true" />
|
|
173
|
+
</a>
|
|
174
|
+
)}
|
|
175
|
+
|
|
153
176
|
{showReaderControls && (
|
|
154
177
|
<DisplaySettings controls={readerPreferences.controls} defaults={readerPreferences.defaults} />
|
|
155
178
|
)}
|
|
@@ -163,6 +186,10 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
163
186
|
{showSiteNavigation && (
|
|
164
187
|
<nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
165
188
|
<h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
|
|
189
|
+
<NavigationTreeControls
|
|
190
|
+
idPrefix="mobile-tree-navigation"
|
|
191
|
+
nodes={siteNavigationTree}
|
|
192
|
+
/>
|
|
166
193
|
<NavigationPageTree
|
|
167
194
|
nodes={siteNavigationTree}
|
|
168
195
|
currentPage={currentPage}
|
|
@@ -172,7 +199,7 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
172
199
|
</nav>
|
|
173
200
|
)}
|
|
174
201
|
|
|
175
|
-
{navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && (
|
|
202
|
+
{navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
|
|
176
203
|
<nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
|
|
177
204
|
{!showSinglePageTopLink && (
|
|
178
205
|
<h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
|
|
@@ -204,7 +231,7 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
204
231
|
</div>
|
|
205
232
|
)}
|
|
206
233
|
|
|
207
|
-
{showPageNavigation && currentPageNavigation && (
|
|
234
|
+
{showPageNavigation && currentPageNavigation && currentPage && (
|
|
208
235
|
<nav
|
|
209
236
|
class:list={['page-nav', { 'page-nav-title-only': showSinglePageTitleOnly }]}
|
|
210
237
|
aria-label={projectConfig.locale.labels.pageNavigation}
|