@janga/norna 0.7.27 → 0.7.29
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 +41 -29
- package/astro.config.mjs +33 -3
- package/bin/norna-cli.mjs +5 -3
- package/package.json +18 -12
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +81 -66
- package/schemas/content-frontmatter.schema.json +23 -7
- package/schemas/manifest.json +49 -3
- package/schemas/page-theme.schema.json +1585 -41
- package/schemas/root-theme.schema.json +1767 -0
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +243 -243
- package/scripts/add-site-node.mjs +31 -250
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +51 -17
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +89 -70
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +70 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +20 -0
- package/scripts/lib/editor-language-service.mjs +74 -73
- package/scripts/lib/editor-page-addresses.mjs +57 -0
- package/scripts/lib/editor-page-placement.mjs +102 -0
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +181 -0
- package/scripts/lib/editor-site-files.mjs +154 -0
- package/scripts/lib/editor-site-links.mjs +81 -0
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +158 -0
- package/scripts/lib/editor-source-files.mjs +87 -0
- package/scripts/lib/editor-theme-help.mjs +43 -0
- package/scripts/lib/locale-registry.mjs +1 -1
- package/scripts/lib/locales/en.mjs +1 -1
- package/scripts/lib/logo-assets.mjs +2 -1
- package/scripts/lib/markdown-images.mjs +59 -0
- package/scripts/lib/markdown-links.mjs +29 -9
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -58
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +8 -1
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +19 -36
- package/scripts/lib/page-move-apply.mjs +119 -0
- package/scripts/lib/page-move-plan.mjs +64 -78
- package/scripts/lib/page-order-source.mjs +22 -0
- package/scripts/lib/page-order.mjs +45 -0
- package/scripts/lib/page-placement-cli.mjs +50 -0
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +10 -8
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +15 -38
- package/scripts/lib/schema-editor-metadata.mjs +16 -60
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +8 -25
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +182 -0
- package/scripts/lib/site-paths.mjs +24 -12
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +86 -95
- package/scripts/lib/site-upgrade.mjs +32 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/lib/theme-config.mjs +26 -68
- package/scripts/lib/theme-packages.mjs +47 -0
- package/scripts/lib/theme-presets.mjs +20 -53
- package/scripts/lib/typography.mjs +2 -2
- package/scripts/lib/yaml-config.mjs +1 -1
- package/scripts/lib/yaml-source-edit.mjs +94 -0
- package/scripts/move-site-page.mjs +46 -254
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +17 -17
- package/scripts/sync-site-public.mjs +44 -66
- package/scripts/upgrade-site.mjs +17 -0
- package/src/components/AreaNavigationMenu.astro +24 -0
- package/src/components/AreaNavigationPanel.astro +55 -0
- package/src/components/AreaNavigationSwitcher.astro +30 -0
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/ImageStackEnhancement.astro +0 -7
- package/src/components/NavigationContinuity.astro +26 -0
- package/src/components/NavigationPageTree.astro +13 -4
- package/src/components/NavigationState.astro +11 -0
- package/src/components/NavigationTreeControls.astro +3 -2
- package/src/components/SearchPage.astro +11 -2
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +38 -61
- package/src/components/SiteSection.astro +1 -0
- package/src/components/SiteTreeNavigation.astro +38 -18
- package/src/components/TopPageMenu.astro +4 -3
- package/src/components/TreeNavigationScript.astro +50 -57
- package/src/content.config.ts +11 -12
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/navigationContinuity.ts +212 -0
- package/src/lib/navigationFollowing.ts +28 -18
- package/src/lib/navigationState.mjs +88 -0
- package/src/lib/navigationStateScript.mjs +76 -0
- package/src/lib/navigationVisibility.ts +9 -0
- package/src/lib/pageThemes.ts +5 -42
- package/src/lib/sectionContent.ts +67 -21
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +13 -36
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +7 -16
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +408 -0
- package/src/styles/content.css +8 -29
- package/src/styles/media.css +24 -0
- package/src/styles/navigation-continuity.css +163 -0
- package/src/styles/page-layout.css +57 -68
- package/src/styles/responsive.css +0 -4
- package/starters/basic/README.md +9 -8
- package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
- package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
- package/schemas/category.schema.json +0 -20
- package/src/components/PageContentsNavigation.astro +0 -20
- /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
- /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -5,84 +5,62 @@ import { createSitemapXml, sitemapFilename } from './lib/sitemap.mjs';
|
|
|
5
5
|
import {
|
|
6
6
|
astroPublicDir,
|
|
7
7
|
astroPublicLabel,
|
|
8
|
-
sitePublicDir,
|
|
9
8
|
sitePublicLabel,
|
|
10
9
|
} from './lib/site-paths.mjs';
|
|
11
10
|
import { getSiteStructure } from './lib/site-structure.mjs';
|
|
12
11
|
import { getSiteLinkGraph } from './lib/site-link-graph.mjs';
|
|
13
12
|
import { assertCategoryDestinationModel } from './lib/category-destinations.mjs';
|
|
14
13
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const main = async () => {
|
|
15
|
+
const readDirectory = async (directory) => {
|
|
16
|
+
try {
|
|
17
|
+
return await readdir(directory, { withFileTypes: true });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error?.code === 'ENOENT') {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
return await readdir(directory, { withFileTypes: true });
|
|
23
|
-
} catch (error) {
|
|
24
|
-
if (error?.code === 'ENOENT') {
|
|
25
|
-
return [];
|
|
23
|
+
throw error;
|
|
26
24
|
}
|
|
25
|
+
};
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
{
|
|
39
|
-
filename: '404.html',
|
|
40
|
-
explanation: 'Norna generates 404.html as the site\'s localized missing-page response.',
|
|
41
|
-
},
|
|
42
|
-
...(projectConfig.search.enabled ? [{
|
|
43
|
-
filename: 'pagefind',
|
|
44
|
-
explanation: 'Norna generates pagefind/ from the rendered editorial content when search is enabled.',
|
|
45
|
-
}] : []),
|
|
46
|
-
];
|
|
47
|
-
for (const generatedFile of generatedPublicFiles) {
|
|
48
|
-
const conflict = sourceEntries.find(({ name }) => name.toLowerCase() === generatedFile.filename);
|
|
49
|
-
if (!conflict) continue;
|
|
50
|
-
|
|
51
|
-
throw new Error([
|
|
52
|
-
`${sitePublicLabel}/${conflict.name} conflicts with Norna's generated ${generatedFile.filename}.`,
|
|
53
|
-
`Remove that source file. ${generatedFile.explanation}`,
|
|
54
|
-
].join('\n'));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const siteStructure = await getSiteStructure();
|
|
58
|
-
const { categoryModel } = await getSiteLinkGraph({ siteStructure });
|
|
59
|
-
const { destinations: categoryDestinations } = assertCategoryDestinationModel(categoryModel);
|
|
60
|
-
const sitemapXml = createSitemapXml({
|
|
61
|
-
siteStructure,
|
|
62
|
-
siteUrl: projectConfig.site.url,
|
|
63
|
-
categoryDestinations,
|
|
64
|
-
});
|
|
27
|
+
const siteStructure = await getSiteStructure();
|
|
28
|
+
const { categoryModel, attachments, diagnostics, publicFilesByPathname } = await getSiteLinkGraph({ siteStructure });
|
|
29
|
+
const outputErrors = diagnostics.filter(issue => !issue.reference && issue.severity === 'error');
|
|
30
|
+
if (outputErrors.length) throw new Error(outputErrors.map(issue => issue.message).join('\n'));
|
|
31
|
+
const { destinations: categoryDestinations } = assertCategoryDestinationModel(categoryModel);
|
|
32
|
+
const sitemapXml = createSitemapXml({
|
|
33
|
+
siteStructure,
|
|
34
|
+
siteUrl: projectConfig.site.url,
|
|
35
|
+
categoryDestinations,
|
|
36
|
+
});
|
|
65
37
|
|
|
66
|
-
await mkdir(astroPublicDir, { recursive: true });
|
|
38
|
+
await mkdir(astroPublicDir, { recursive: true });
|
|
67
39
|
|
|
68
|
-
for (const entry of await readDirectory(astroPublicDir)) {
|
|
69
|
-
|
|
70
|
-
|
|
40
|
+
for (const entry of await readDirectory(astroPublicDir)) {
|
|
41
|
+
if (entry.name !== '_norna') await rm(path.join(astroPublicDir, entry.name), { force: true, recursive: true });
|
|
42
|
+
}
|
|
43
|
+
const resources = path.join(astroPublicDir, '_norna');
|
|
44
|
+
for (const entry of await readDirectory(resources)) {
|
|
45
|
+
if (entry.name === 'images' || entry.name === 'search' && projectConfig.search.enabled) continue;
|
|
46
|
+
await rm(path.join(resources, entry.name), { force: true, recursive: true });
|
|
47
|
+
}
|
|
48
|
+
for (const file of new Set(publicFilesByPathname.values())) {
|
|
49
|
+
const destination = path.join(astroPublicDir, file.pathname.slice(1));
|
|
50
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
51
|
+
await cp(file.filePath, destination, { force: false, errorOnExist: true });
|
|
71
52
|
}
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
path.join(astroPublicDir, entry.name),
|
|
80
|
-
{ force: true, recursive: true },
|
|
81
|
-
);
|
|
82
|
-
}
|
|
54
|
+
for (const file of attachments.files) {
|
|
55
|
+
const destination = path.join(astroPublicDir, file.pathname.slice(1));
|
|
56
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
57
|
+
await cp(file.filePath, destination, { force: false, errorOnExist: true });
|
|
58
|
+
}
|
|
59
|
+
await writeFile(path.join(astroPublicDir, sitemapFilename), sitemapXml);
|
|
83
60
|
|
|
84
|
-
|
|
61
|
+
console.log(`Synced ${sitePublicLabel}/ to ${astroPublicLabel}/.`);
|
|
62
|
+
const pageCount = siteStructure.contentFiles.length + categoryDestinations.filter(({ kind }) => kind === 'listing').length;
|
|
63
|
+
console.log(`Generated ${astroPublicLabel}/${sitemapFilename} for ${pageCount} public page${pageCount === 1 ? '' : 's'}.`);
|
|
64
|
+
};
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
const pageCount = siteStructure.contentFiles.length + categoryDestinations.filter(({ kind }) => kind === 'listing').length;
|
|
88
|
-
console.log(`Generated ${astroPublicLabel}/${sitemapFilename} for ${pageCount} public page${pageCount === 1 ? '' : 's'}.`);
|
|
66
|
+
await main().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { siteDir } from './lib/site-paths.mjs';
|
|
2
|
+
import { planSiteUpgrade } from './lib/site-upgrade.mjs';
|
|
3
|
+
|
|
4
|
+
const usage = 'Usage: norna site:upgrade [--apply]\nCheck the root/ source layout. Former layouts require manual conversion; --apply is retained but does not convert old sources.';
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
try {
|
|
7
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
+
console.log(usage);
|
|
9
|
+
} else {
|
|
10
|
+
if (args.length > 1 || args.some((arg) => arg !== '--apply')) throw new Error(usage);
|
|
11
|
+
await planSiteUpgrade(siteDir);
|
|
12
|
+
console.log('The site already uses the current configuration and homepage layout. No files changed.');
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(error.message);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import type { CategoryDestinationMap, SiteNavigationNode } from '../lib/siteNavigation';
|
|
4
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
5
|
+
import AreaNavigationPanel from './AreaNavigationPanel.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
node: SiteNavigationNode;
|
|
9
|
+
currentPage?: SiteNode;
|
|
10
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
11
|
+
}
|
|
12
|
+
const { node, currentPage, categoryDestinations } = Astro.props;
|
|
13
|
+
const label = `${projectConfig.locale.labels.navigationMenu}: ${node.node.title}`;
|
|
14
|
+
const panelId = `area-menu-${encodeURIComponent(node.node.pagePath)}`;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
{node.children.length > 0 && (
|
|
18
|
+
<details class="top-page-menu area-navigation-menu" data-area-menu={node.node.pagePath}>
|
|
19
|
+
<summary aria-label={label} aria-controls={panelId}>
|
|
20
|
+
<span class="top-page-menu-title">{node.node.title}</span>
|
|
21
|
+
</summary>
|
|
22
|
+
<AreaNavigationPanel id={panelId} node={node} currentPage={currentPage} categoryDestinations={categoryDestinations} />
|
|
23
|
+
</details>
|
|
24
|
+
)}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Check } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
import { withBasePath } from '../lib/basePath';
|
|
5
|
+
import { getNavigationDestination, type CategoryDestinationMap, type SiteNavigationNode } from '../lib/siteNavigation';
|
|
6
|
+
import { getAreaMenuGroups, resolveAreaNavigation } from '../lib/areaNavigation';
|
|
7
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
id: string;
|
|
11
|
+
node?: SiteNavigationNode;
|
|
12
|
+
currentPage?: SiteNode;
|
|
13
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
14
|
+
otherRoots?: SiteNavigationNode[];
|
|
15
|
+
}
|
|
16
|
+
const { id, node, currentPage, categoryDestinations, otherRoots = [] } = Astro.props;
|
|
17
|
+
const { localRoot } = resolveAreaNavigation(node ? [node] : [], currentPage);
|
|
18
|
+
const { branches, pages } = node ? getAreaMenuGroups(node) : { branches: [], pages: [] };
|
|
19
|
+
const columnsFor = (count: number, maximum: number) => count === 0 ? 0 : Math.ceil(count / Math.ceil(count / maximum));
|
|
20
|
+
const branchColumns = columnsFor(branches.length, pages.length > 0 ? 2 : 3);
|
|
21
|
+
const pageColumns = branches.length === 0 ? columnsFor(pages.length, 3) : pages.length > 0 ? 1 : 0;
|
|
22
|
+
const groups = [
|
|
23
|
+
{ kind: 'branches', choices: branches, columns: branchColumns },
|
|
24
|
+
{ kind: 'pages', choices: pages, columns: pageColumns },
|
|
25
|
+
].filter(({ choices }) => choices.length > 0);
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<nav id={id} class="top-page-panel area-navigation-panel"
|
|
29
|
+
style={`--area-menu-columns: ${branchColumns + pageColumns || 2}`}
|
|
30
|
+
aria-label={node ? `${node.node.title}: ${projectConfig.locale.labels.navigationChildren}` : projectConfig.locale.labels.siteNavigation}>
|
|
31
|
+
{groups.map((group) => <ul class="area-navigation-choices" data-area-choice-group={group.kind}
|
|
32
|
+
style={`--area-group-columns: ${group.columns}; --area-group-rows: ${Math.ceil(group.choices.length / group.columns)}`}>
|
|
33
|
+
{group.choices.map((child) => {
|
|
34
|
+
const destination = getNavigationDestination(child.node, categoryDestinations);
|
|
35
|
+
const selected = localRoot === child || currentPage?.pagePath === child.node.pagePath;
|
|
36
|
+
const description = child.node.kind === 'page'
|
|
37
|
+
? child.node.entry.data.page?.description
|
|
38
|
+
: child.node.description;
|
|
39
|
+
return <li>
|
|
40
|
+
<a href={withBasePath(projectConfig.site.basePath, destination.pathname)}
|
|
41
|
+
aria-current={selected ? 'true' : undefined} data-area-choice={child.node.pagePath}>
|
|
42
|
+
<span class="area-navigation-title">{child.node.title}{selected && <Check aria-hidden="true" />}</span>
|
|
43
|
+
{group.kind === 'branches' && description && <span class="area-navigation-description">{description}</span>}
|
|
44
|
+
</a>
|
|
45
|
+
</li>;
|
|
46
|
+
})}
|
|
47
|
+
</ul>)}
|
|
48
|
+
{otherRoots.length > 0 && <ul class="area-navigation-other-roots" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
49
|
+
{otherRoots.map(({ node: root }) => <li>
|
|
50
|
+
<a href={withBasePath(projectConfig.site.basePath, getNavigationDestination(root, categoryDestinations).pathname)}>
|
|
51
|
+
{root.isHome ? projectConfig.locale.labels.returnHome : root.title}
|
|
52
|
+
</a>
|
|
53
|
+
</li>)}
|
|
54
|
+
</ul>}
|
|
55
|
+
</nav>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Menu } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
import { hasLargeAreaMenu, resolveAreaNavigation } from '../lib/areaNavigation';
|
|
5
|
+
import type { CategoryDestinationMap, SiteNavigationNode } from '../lib/siteNavigation';
|
|
6
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
7
|
+
import AreaNavigationPanel from './AreaNavigationPanel.astro';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
nodes: SiteNavigationNode[];
|
|
11
|
+
currentPage?: SiteNode;
|
|
12
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { nodes, currentPage, categoryDestinations, label } = Astro.props;
|
|
17
|
+
const { globalRoot } = resolveAreaNavigation(nodes, currentPage);
|
|
18
|
+
const currentLabel = label ?? globalRoot?.node.title ?? projectConfig.locale.labels.navigationMenu;
|
|
19
|
+
const menuRoot = globalRoot && globalRoot.children.length > 0 && !hasLargeAreaMenu(globalRoot) ? globalRoot : undefined;
|
|
20
|
+
const otherRoots = nodes.filter((root) => root !== menuRoot);
|
|
21
|
+
const menuLabel = `${projectConfig.locale.labels.navigationMenu}: ${currentLabel}`;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<details class="top-page-menu area-navigation-switcher" data-area-switcher>
|
|
25
|
+
<summary aria-label={menuLabel} title={menuLabel} aria-controls="area-switcher-panel">
|
|
26
|
+
<Menu aria-hidden="true" />
|
|
27
|
+
</summary>
|
|
28
|
+
<AreaNavigationPanel id="area-switcher-panel" node={menuRoot} currentPage={currentPage}
|
|
29
|
+
categoryDestinations={categoryDestinations} otherRoots={otherRoots} />
|
|
30
|
+
</details>
|
|
@@ -4,6 +4,7 @@ import ImageCarousel from './ImageCarousel.astro';
|
|
|
4
4
|
import ImageStack from './ImageStack.astro';
|
|
5
5
|
import PageList from './PageList.astro';
|
|
6
6
|
import type { SectionContentBlock } from '../lib/sectionContent';
|
|
7
|
+
import { renderNestedMarkdownImages } from '../lib/markdownImages';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
blocks: SectionContentBlock[];
|
|
@@ -20,7 +21,7 @@ const firstMediaIndex = blocks.findIndex((block) => block.type === 'image-stack'
|
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
{blocks.map((block, index) => {
|
|
23
|
-
if (block.type === 'html') return <div class="section-markdown" set:html={block.html} />;
|
|
24
|
+
if (block.type === 'html') return <div class="section-markdown" set:html={renderNestedMarkdownImages(block.html, title, images.maxAvailableWidthPercent)} />;
|
|
24
25
|
if (block.type === 'tabs') return (
|
|
25
26
|
<div class="content-tabs content-block-note-lane-boundary" data-content-tabs>
|
|
26
27
|
<div class="content-tabs-controls" data-tabs-controls hidden></div>
|
|
@@ -43,10 +43,11 @@ const managedImageSizes = getManagedImageSizes(maxAvailableWidthPercent);
|
|
|
43
43
|
<div class="managed-image-frame">
|
|
44
44
|
<a
|
|
45
45
|
class="managed-image-inspection-link"
|
|
46
|
-
href={inspection.href}
|
|
47
|
-
|
|
46
|
+
href={image.href ?? inspection.href}
|
|
47
|
+
title={image.linkTitle}
|
|
48
|
+
aria-label={image.href === undefined ? inspectionLabel : undefined}
|
|
48
49
|
aria-describedby={captionId}
|
|
49
|
-
data-image-inspection-trigger
|
|
50
|
+
data-image-inspection-trigger={image.href === undefined ? '' : undefined}
|
|
50
51
|
data-image-intrinsic-width={inspection.width}
|
|
51
52
|
data-image-intrinsic-height={inspection.height}
|
|
52
53
|
data-image-inspection-scalable={inspection.scalable ? 'true' : undefined}
|
|
@@ -56,12 +57,13 @@ const managedImageSizes = getManagedImageSizes(maxAvailableWidthPercent);
|
|
|
56
57
|
{...getImageAttributes(image.src ?? image.image, managedImageSizes)}
|
|
57
58
|
class:list={['managed-image', getImageClass(image)]}
|
|
58
59
|
alt={image.alt ?? ''}
|
|
60
|
+
title={image.title}
|
|
59
61
|
aria-describedby={captionId}
|
|
60
62
|
{...getImageLoadingProps(isPriorityItem)}
|
|
61
63
|
/>
|
|
62
|
-
<span class="managed-image-inspection-affordance" aria-hidden="true">
|
|
64
|
+
{image.href === undefined && <span class="managed-image-inspection-affordance" aria-hidden="true">
|
|
63
65
|
<Maximize2 size={18} stroke-width={2} />
|
|
64
|
-
</span>
|
|
66
|
+
</span>}
|
|
65
67
|
</a>
|
|
66
68
|
</div>
|
|
67
69
|
{image.caption && (
|
|
@@ -104,13 +104,6 @@ const labels = projectConfig.locale.labels;
|
|
|
104
104
|
const imageBounds = image.getBoundingClientRect();
|
|
105
105
|
if (imageBounds.height < usableViewportHeight * persistentCaptionHeightRatio) return;
|
|
106
106
|
|
|
107
|
-
const visibleContentsRail = Array.from(
|
|
108
|
-
layout.querySelectorAll<HTMLElement>('.page-contents-navigation-rail'),
|
|
109
|
-
).some(isVisible);
|
|
110
|
-
const reservesContentsLane = layout.dataset.pageContentsPlacement === 'contents-rail'
|
|
111
|
-
&& document.documentElement.dataset.focusReading !== 'on';
|
|
112
|
-
if (visibleContentsRail || reservesContentsLane) return;
|
|
113
|
-
|
|
114
107
|
const frameBounds = frame.getBoundingClientRect();
|
|
115
108
|
const figureBounds = figure.getBoundingClientRect();
|
|
116
109
|
const captionBounds = caption.getBoundingClientRect();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import styles from '../styles/navigation-continuity.css?inline';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<style is:inline set:html={styles} />
|
|
6
|
+
<script is:inline>
|
|
7
|
+
// A cold history arrival can restore its position at pagereveal, then lose
|
|
8
|
+
// it to native fragment scrolling when deferred modules finish parsing.
|
|
9
|
+
// Keep the browser's restored position until that initial load completes.
|
|
10
|
+
window.addEventListener('pagereveal', () => {
|
|
11
|
+
if (!location.hash || document.readyState === 'complete'
|
|
12
|
+
|| performance.getEntriesByType('navigation')[0]?.type !== 'back_forward'
|
|
13
|
+
|| history.scrollRestoration !== 'auto') return;
|
|
14
|
+
|
|
15
|
+
history.scrollRestoration = 'manual';
|
|
16
|
+
const release = () => {
|
|
17
|
+
history.scrollRestoration = 'auto';
|
|
18
|
+
window.removeEventListener('load', release);
|
|
19
|
+
window.removeEventListener('pagehide', release);
|
|
20
|
+
};
|
|
21
|
+
window.addEventListener('load', release);
|
|
22
|
+
window.addEventListener('pagehide', release);
|
|
23
|
+
}, { once: true });
|
|
24
|
+
</script>
|
|
25
|
+
<!-- A streamed response must finish the page before its transition snapshot. -->
|
|
26
|
+
<link rel="expect" blocking="render" href="#norna-navigation-ready" />
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { withBasePath } from '../lib/basePath';
|
|
4
|
-
import type
|
|
4
|
+
import { getNavigationDestination, type CategoryDestinationMap, type SiteNavigationNode } from '../lib/siteNavigation';
|
|
5
5
|
import type { SiteNode, SitePage } from '../lib/sitePages';
|
|
6
6
|
import PageContentsLinks from './PageContentsLinks.astro';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
nodes: SiteNavigationNode[];
|
|
10
10
|
currentPage?: SiteNode;
|
|
11
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
11
12
|
variant: 'mobile' | 'sidebar' | 'submenu';
|
|
12
13
|
integratePageSections?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const { nodes, currentPage, variant, integratePageSections = false } = Astro.props;
|
|
16
|
+
const { nodes, currentPage, categoryDestinations, variant, integratePageSections = false } = Astro.props;
|
|
16
17
|
const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
18
|
+
const categoryHref = (node: SiteNavigationNode) => withBasePath(
|
|
19
|
+
projectConfig.site.basePath, getNavigationDestination(node.node, categoryDestinations).pathname,
|
|
20
|
+
);
|
|
17
21
|
const hasBranch = (node: SiteNavigationNode) => node.children.length > 0;
|
|
18
22
|
const isCurrentPage = (page: SitePage) => currentPage?.pathname === page.pathname;
|
|
19
23
|
const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
|
|
@@ -71,13 +75,17 @@ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' &
|
|
|
71
75
|
class="navigation-page-summary"
|
|
72
76
|
aria-controls={getBranchId(node.node)}
|
|
73
77
|
>
|
|
74
|
-
|
|
78
|
+
<a class:list={['navigation-page-summary-title', 'navigation-category-link', {
|
|
79
|
+
'mobile-nav-destination': variant === 'mobile',
|
|
80
|
+
}]}
|
|
81
|
+
href={categoryHref(node)}>{node.node.title}</a>
|
|
75
82
|
<span class="navigation-page-chevron" aria-hidden="true"></span>
|
|
76
83
|
</summary>
|
|
77
84
|
<div id={getBranchId(node.node)} class="navigation-page-branch-content">
|
|
78
85
|
<Astro.self
|
|
79
86
|
nodes={node.children}
|
|
80
87
|
currentPage={currentPage}
|
|
88
|
+
categoryDestinations={categoryDestinations}
|
|
81
89
|
variant={variant}
|
|
82
90
|
integratePageSections={integratePageSections}
|
|
83
91
|
/>
|
|
@@ -123,6 +131,7 @@ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' &
|
|
|
123
131
|
<Astro.self
|
|
124
132
|
nodes={node.children}
|
|
125
133
|
currentPage={currentPage}
|
|
134
|
+
categoryDestinations={categoryDestinations}
|
|
126
135
|
variant={variant}
|
|
127
136
|
integratePageSections={integratePageSections}
|
|
128
137
|
/>
|
|
@@ -174,7 +183,7 @@ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' &
|
|
|
174
183
|
{node.node.title}
|
|
175
184
|
</a>
|
|
176
185
|
{node.children.length > 0 && (
|
|
177
|
-
<Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
|
|
186
|
+
<Astro.self nodes={node.children} currentPage={currentPage} categoryDestinations={categoryDestinations} variant={variant} />
|
|
178
187
|
)}
|
|
179
188
|
</>
|
|
180
189
|
)}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getNavigationStateScript } from '../lib/navigationStateScript.mjs';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
scope: 'desktop' | 'mobile';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { scope } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<script is:inline data-navigation-state-scope={scope} set:html={getNavigationStateScript()} />
|
|
@@ -6,15 +6,16 @@ import type { SiteNavigationNode } from '../lib/siteNavigation';
|
|
|
6
6
|
interface Props {
|
|
7
7
|
idPrefix: string;
|
|
8
8
|
nodes: SiteNavigationNode[];
|
|
9
|
+
forceVisible?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const { idPrefix, nodes } = Astro.props;
|
|
12
|
+
const { idPrefix, nodes, forceVisible = false } = Astro.props;
|
|
12
13
|
const countNodes = (tree: SiteNavigationNode[]): number => tree.reduce(
|
|
13
14
|
(total, node) => total + 1 + countNodes(node.children),
|
|
14
15
|
0,
|
|
15
16
|
);
|
|
16
17
|
const minimumUsefulNodeCount = 12;
|
|
17
|
-
const showControls = countNodes(nodes) >= minimumUsefulNodeCount;
|
|
18
|
+
const showControls = forceVisible || countNodes(nodes) >= minimumUsefulNodeCount;
|
|
18
19
|
const labels = projectConfig.locale.labels;
|
|
19
20
|
const filterId = `${idPrefix}-filter`;
|
|
20
21
|
const statusId = `${idPrefix}-status`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { searchOutputDirectory } from '../../scripts/lib/publication-paths.mjs';
|
|
2
3
|
import { getCollection } from 'astro:content';
|
|
3
4
|
import { ArrowLeft } from '@lucide/astro';
|
|
4
5
|
import { resolveNavigationModel } from '../../scripts/lib/navigation-model.mjs';
|
|
@@ -17,12 +18,13 @@ import { withBasePath } from '../lib/basePath';
|
|
|
17
18
|
|
|
18
19
|
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
19
20
|
const siteModel = await getSiteModel(await getCollection('site'));
|
|
21
|
+
const categoryDestinations = siteModel.categoryDestinations.byPathname;
|
|
20
22
|
const { nodes: siteNodes, pages: sitePages } = siteModel;
|
|
21
23
|
const homePage = sitePages.find((page) => page.isHome);
|
|
22
24
|
if (!homePage) throw new Error('The required homepage is missing from the site page model.');
|
|
23
25
|
|
|
24
26
|
const [siteTheme] = await getCollection('theme');
|
|
25
|
-
if (!siteTheme) throw new Error('site/theme.yaml is required. Add
|
|
27
|
+
if (!siteTheme) throw new Error('site/root/tree-theme.yaml is required. Add the shared site theme before building the site.');
|
|
26
28
|
const [sitewide] = await getCollection('sitewide');
|
|
27
29
|
const sitewideData = sitewide?.data ?? {};
|
|
28
30
|
const siteThemeData = siteTheme.data;
|
|
@@ -55,6 +57,7 @@ const pagePresentation = resolvePagePresentation(siteThemeData, themeLabel, {
|
|
|
55
57
|
navigationMode: navigationModel.mode,
|
|
56
58
|
});
|
|
57
59
|
const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
|
|
60
|
+
const areaReadingFrame = navigationModel.mode === 'tree';
|
|
58
61
|
const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: themeLabel });
|
|
59
62
|
const logoAsset = getLogoAsset();
|
|
60
63
|
const logo = logoAsset
|
|
@@ -82,7 +85,7 @@ const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
|
|
|
82
85
|
navigationModel.requestedMode === 'automatic'
|
|
83
86
|
&& (navigationModel.hasNestedPages || navigationModel.hasCategories)
|
|
84
87
|
);
|
|
85
|
-
const bundlePath = withBasePath(projectConfig.site.basePath,
|
|
88
|
+
const bundlePath = withBasePath(projectConfig.site.basePath, `/${searchOutputDirectory}/`);
|
|
86
89
|
const pagefindTranslationOption = projectConfig.locale.pagefindTranslations
|
|
87
90
|
? `,\n\t\t\ttranslations: ${JSON.stringify(projectConfig.locale.pagefindTranslations)}`
|
|
88
91
|
: '';
|
|
@@ -130,19 +133,24 @@ const searchScript = `(() => {
|
|
|
130
133
|
pathname="/search/"
|
|
131
134
|
footer={sitewideData.footer}
|
|
132
135
|
indexable={false}
|
|
136
|
+
areaNavigation={navigationModel.mode === 'tree'}
|
|
137
|
+
areaReadingFrame={areaReadingFrame}
|
|
133
138
|
>
|
|
134
139
|
<link slot="head" rel="stylesheet" href={`${bundlePath}pagefind-ui.css`} />
|
|
135
140
|
<SiteNavigation
|
|
136
141
|
logo={logo}
|
|
137
142
|
navigationEntries={siteNavigation}
|
|
143
|
+
categoryDestinations={categoryDestinations}
|
|
138
144
|
navigationMode={navigationModel.mode}
|
|
139
145
|
showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
|
|
140
146
|
hasNavigationRailFrame={hasNavigationRailFrame}
|
|
141
147
|
frameColors={frameColors}
|
|
142
148
|
readerPreferences={pagePresentation.readerPreferences}
|
|
143
149
|
searchPage
|
|
150
|
+
areaReadingFrame={areaReadingFrame}
|
|
144
151
|
/>
|
|
145
152
|
{siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
|
|
153
|
+
<div class:list={['site-page-layout', { 'site-page-layout-tree': areaReadingFrame }]}>
|
|
146
154
|
<main class="site-content search-page-content" id="main-content" style={`--page-text-width: ${pageTextWidth}`}>
|
|
147
155
|
<SiteSection
|
|
148
156
|
section={searchSection}
|
|
@@ -158,6 +166,7 @@ const searchScript = `(() => {
|
|
|
158
166
|
</div>
|
|
159
167
|
</SiteSection>
|
|
160
168
|
</main>
|
|
169
|
+
</div>
|
|
161
170
|
<script is:inline set:html={searchScript} />
|
|
162
171
|
</BaseLayout>
|
|
163
172
|
|