@janga/norna 0.7.27 → 0.7.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -28
- package/astro.config.mjs +19 -1
- package/bin/norna-cli.mjs +5 -3
- package/package.json +9 -5
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +66 -66
- package/schemas/content-frontmatter.schema.json +16 -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 +13 -161
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +17 -10
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +7 -6
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +67 -60
- package/scripts/lib/docusaurus-migration-audit.mjs +9 -6
- package/scripts/lib/editor-file-policy.mjs +18 -0
- package/scripts/lib/editor-language-service.mjs +53 -55
- package/scripts/lib/editor-page-addresses.mjs +55 -0
- package/scripts/lib/editor-page-placement.mjs +172 -0
- package/scripts/lib/editor-site-editing-tree.mjs +146 -0
- package/scripts/lib/editor-site-files.mjs +153 -0
- package/scripts/lib/editor-site-links.mjs +79 -0
- package/scripts/lib/editor-site-tree.mjs +151 -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/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -1
- package/scripts/lib/page-aliases.mjs +3 -1
- package/scripts/lib/page-model.mjs +17 -20
- package/scripts/lib/page-move-apply.mjs +117 -0
- package/scripts/lib/page-move-plan.mjs +27 -59
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +7 -8
- package/scripts/lib/schema-definitions.mjs +11 -38
- package/scripts/lib/schema-editor-metadata.mjs +12 -59
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +7 -24
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +169 -0
- package/scripts/lib/site-paths.mjs +22 -10
- package/scripts/lib/site-structure.mjs +72 -82
- package/scripts/lib/site-upgrade.mjs +30 -0
- package/scripts/lib/source-files.mjs +34 -0
- 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 +3 -124
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +9 -7
- package/scripts/sync-site-public.mjs +1 -1
- 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/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 +9 -1
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +37 -61
- 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 +10 -8
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -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 +3 -2
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +9 -6
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +5 -15
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +399 -0
- package/src/styles/content.css +2 -29
- 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/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/{config.yaml → site-config/settings.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
---
|
|
2
|
+
import NavigationState from './NavigationState.astro';
|
|
2
3
|
import { Menu, Search, X } from '@lucide/astro';
|
|
3
4
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
5
|
import { withBasePath } from '../lib/basePath';
|
|
5
6
|
import type { SectionNavigation } from '../lib/sectionContent';
|
|
6
7
|
import {
|
|
7
8
|
getListedSiteNavigationTree,
|
|
9
|
+
getNavigationDestination,
|
|
10
|
+
type CategoryDestinationMap,
|
|
8
11
|
type SiteNavigationEntry,
|
|
9
|
-
type SiteNavigationNode,
|
|
10
12
|
} from '../lib/siteNavigation';
|
|
11
13
|
import type { SiteNode } from '../lib/sitePages';
|
|
12
14
|
import DisplaySettings from './DisplaySettings.astro';
|
|
13
15
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
14
16
|
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
15
17
|
import TopPageMenu from './TopPageMenu.astro';
|
|
18
|
+
import AreaNavigationMenu from './AreaNavigationMenu.astro';
|
|
19
|
+
import AreaNavigationSwitcher from './AreaNavigationSwitcher.astro';
|
|
20
|
+
import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
|
|
21
|
+
import { hasLargeAreaMenu, withH2Outlines } from '../lib/areaNavigation';
|
|
16
22
|
import SectionNavigationScript from './SectionNavigationScript.astro';
|
|
17
23
|
import SearchNavigationScript from './SearchNavigationScript.astro';
|
|
18
24
|
|
|
@@ -29,6 +35,7 @@ interface Props {
|
|
|
29
35
|
};
|
|
30
36
|
currentPage?: SiteNode;
|
|
31
37
|
navigationEntries: SiteNavigationEntry[];
|
|
38
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
32
39
|
navigationMode: 'sections' | 'top' | 'tree';
|
|
33
40
|
showTopNavigationSubmenus: boolean;
|
|
34
41
|
hasNavigationRailFrame: boolean;
|
|
@@ -46,18 +53,21 @@ interface Props {
|
|
|
46
53
|
};
|
|
47
54
|
};
|
|
48
55
|
searchPage?: boolean;
|
|
56
|
+
areaReadingFrame?: boolean;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
const {
|
|
52
60
|
logo,
|
|
53
61
|
currentPage,
|
|
54
62
|
navigationEntries,
|
|
63
|
+
categoryDestinations,
|
|
55
64
|
navigationMode,
|
|
56
65
|
showTopNavigationSubmenus,
|
|
57
66
|
hasNavigationRailFrame,
|
|
58
67
|
frameColors,
|
|
59
68
|
readerPreferences,
|
|
60
69
|
searchPage = false,
|
|
70
|
+
areaReadingFrame = false,
|
|
61
71
|
} = Astro.props;
|
|
62
72
|
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
63
73
|
? 'var(--color-page)'
|
|
@@ -68,12 +78,7 @@ const navigationStyle = [
|
|
|
68
78
|
...(logo?.height ? [`--site-brand-logo-height: ${logo.height}`] : []),
|
|
69
79
|
].join('; ');
|
|
70
80
|
const siteNavigationTree = getListedSiteNavigationTree(navigationEntries);
|
|
71
|
-
const mobileNavigationTree =
|
|
72
|
-
? getListedSiteNavigationTree(navigationEntries.map((entry) => ({
|
|
73
|
-
...entry,
|
|
74
|
-
headings: entry.headings.filter((heading) => heading.depth === 2),
|
|
75
|
-
})))
|
|
76
|
-
: siteNavigationTree;
|
|
81
|
+
const mobileNavigationTree = withH2Outlines(siteNavigationTree);
|
|
77
82
|
const topLevelNavigation = siteNavigationTree;
|
|
78
83
|
const showSiteNavigation = topLevelNavigation.some(({ node }) => !node.isHome);
|
|
79
84
|
const homeNode = navigationEntries.find(({ node }) => node.isHome)?.node;
|
|
@@ -88,7 +93,6 @@ const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
|
|
|
88
93
|
|| currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
|
|
89
94
|
));
|
|
90
95
|
const pageHref = (page: SiteNode) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
91
|
-
const getNavigationPage = (node: SiteNavigationNode) => node.node;
|
|
92
96
|
const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
|
|
93
97
|
const showSinglePageTopLink = !showSiteNavigation;
|
|
94
98
|
const currentPageSectionCount = currentPageNavigation?.sections.length ?? 0;
|
|
@@ -131,35 +135,41 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
131
135
|
>
|
|
132
136
|
{showNavigationRow && (
|
|
133
137
|
<div class:list={['site-nav-row', { 'site-nav-row-mobile-only': !showDesktopNavigationRow }]}>
|
|
134
|
-
{logo && (
|
|
138
|
+
{(logo || areaReadingFrame) && (
|
|
135
139
|
<div class="site-nav-leading">
|
|
136
|
-
<a class="site-brand" href={pageHref(homePage)}>
|
|
137
|
-
<img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} />
|
|
140
|
+
<a class="site-brand" href={pageHref(homePage)} aria-label={projectConfig.locale.labels.returnHome} title={projectConfig.locale.labels.returnHome}>
|
|
141
|
+
{logo ? <img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} /> : homePage.title}
|
|
138
142
|
</a>
|
|
139
143
|
</div>
|
|
140
144
|
)}
|
|
141
145
|
|
|
142
146
|
<div class="site-nav-actions">
|
|
143
|
-
{showSiteNavigation && (
|
|
147
|
+
{showSiteNavigation && !areaReadingFrame && (
|
|
144
148
|
<nav class="site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
145
149
|
<ul>
|
|
146
150
|
{topLevelNavigation.map((navigationNode) => {
|
|
147
|
-
const destination =
|
|
151
|
+
const destination = getNavigationDestination(navigationNode.node, categoryDestinations);
|
|
148
152
|
if (!destination) return null;
|
|
149
153
|
const nodeIsCurrentPage = isCurrentPage(navigationNode.node);
|
|
154
|
+
const useAreaMenu = navigationMode === 'tree';
|
|
150
155
|
return (
|
|
151
156
|
<li class:list={['site-nav-item', {
|
|
152
|
-
'site-nav-item-current-branch': !nodeIsCurrentPage && isCurrentBranch(navigationNode.node),
|
|
157
|
+
'site-nav-item-current-branch': (!nodeIsCurrentPage || useAreaMenu) && isCurrentBranch(navigationNode.node),
|
|
153
158
|
}]}>
|
|
154
|
-
|
|
159
|
+
{useAreaMenu && navigationNode.children.length > 0 && !hasLargeAreaMenu(navigationNode) ? <AreaNavigationMenu
|
|
160
|
+
node={navigationNode}
|
|
161
|
+
currentPage={currentPage}
|
|
162
|
+
categoryDestinations={categoryDestinations}
|
|
163
|
+
/> : <a href={pageHref(destination)} aria-current={nodeIsCurrentPage ? 'page' : undefined}>
|
|
155
164
|
{navigationNode.node.title}
|
|
156
|
-
</a>
|
|
157
|
-
<TopPageMenu
|
|
165
|
+
</a>}
|
|
166
|
+
{!useAreaMenu && <TopPageMenu
|
|
158
167
|
node={navigationNode}
|
|
159
168
|
currentPage={currentPage}
|
|
169
|
+
categoryDestinations={categoryDestinations}
|
|
160
170
|
showSections={navigationMode === 'top'}
|
|
161
171
|
showChildren={showTopNavigationSubmenus}
|
|
162
|
-
/>
|
|
172
|
+
/>}
|
|
163
173
|
</li>
|
|
164
174
|
);
|
|
165
175
|
})}
|
|
@@ -211,9 +221,10 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
211
221
|
</div>
|
|
212
222
|
<div
|
|
213
223
|
class="mobile-site-nav"
|
|
214
|
-
data-navigation-root={pageHref(homePage)}
|
|
215
|
-
data-navigation-state-root={pageHref(homePage)}
|
|
224
|
+
data-navigation-root={`area:${pageHref(homePage)}`}
|
|
225
|
+
data-navigation-state-root={`area:${pageHref(homePage)}`}
|
|
216
226
|
>
|
|
227
|
+
<NavigationState scope="mobile" />
|
|
217
228
|
{showSiteNavigation && (
|
|
218
229
|
<nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
219
230
|
<h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
|
|
@@ -224,6 +235,7 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
224
235
|
<NavigationPageTree
|
|
225
236
|
nodes={mobileNavigationTree}
|
|
226
237
|
currentPage={currentPage}
|
|
238
|
+
categoryDestinations={categoryDestinations}
|
|
227
239
|
variant="mobile"
|
|
228
240
|
integratePageSections={navigationMode === 'tree' || navigationMode === 'top'}
|
|
229
241
|
/>
|
|
@@ -260,6 +272,19 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
260
272
|
</details>
|
|
261
273
|
)}
|
|
262
274
|
</div>
|
|
275
|
+
{areaReadingFrame && (
|
|
276
|
+
<div class="site-reading-location">
|
|
277
|
+
<AreaNavigationSwitcher
|
|
278
|
+
nodes={siteNavigationTree}
|
|
279
|
+
currentPage={currentPage}
|
|
280
|
+
categoryDestinations={categoryDestinations}
|
|
281
|
+
label={searchPage ? projectConfig.locale.labels.search : undefined}
|
|
282
|
+
/>
|
|
283
|
+
{currentPage
|
|
284
|
+
? <SiteBreadcrumbs currentPage={currentPage} nodes={navigationEntries.map(({ node }) => node)} />
|
|
285
|
+
: <span class="site-reading-location-label">{projectConfig.locale.labels.search}</span>}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
263
288
|
</div>
|
|
264
289
|
)}
|
|
265
290
|
|
|
@@ -9,7 +9,6 @@ import ImageStackEnhancement from './ImageStackEnhancement.astro';
|
|
|
9
9
|
import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
|
|
10
10
|
import SiteNavigation from './SiteNavigation.astro';
|
|
11
11
|
import SiteBanners from './SiteBanners.astro';
|
|
12
|
-
import PageContentsNavigation from './PageContentsNavigation.astro';
|
|
13
12
|
import PageSequenceNavigation from './PageSequenceNavigation.astro';
|
|
14
13
|
import SiteSection from './SiteSection.astro';
|
|
15
14
|
import SiteTreeNavigation from './SiteTreeNavigation.astro';
|
|
@@ -22,47 +21,37 @@ import {
|
|
|
22
21
|
flattenSiteNavigationTree,
|
|
23
22
|
getDirectChildPages,
|
|
24
23
|
getListedSiteNavigationTree,
|
|
24
|
+
getNavigationDestination,
|
|
25
25
|
getSequentialPageNavigation,
|
|
26
26
|
} from '../lib/siteNavigation';
|
|
27
27
|
import { getToday, isVisibleToday } from '../lib/visibility';
|
|
28
28
|
import { getLogoAsset } from '../lib/sitePublicAssets';
|
|
29
29
|
import {
|
|
30
30
|
resolveNavigationModel,
|
|
31
|
-
resolvePageContentsPlacement,
|
|
32
31
|
} from '../../scripts/lib/navigation-model.mjs';
|
|
33
32
|
import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
|
|
34
33
|
import { resolveEditSourceTarget } from '../../scripts/lib/edit-source-link.mjs';
|
|
35
34
|
import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
36
|
-
import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
|
|
37
35
|
|
|
38
36
|
type SiteEntry = CollectionEntry<'site'>;
|
|
39
37
|
|
|
40
38
|
interface Props {
|
|
41
39
|
entry?: SiteEntry;
|
|
42
|
-
categoryPath?: string;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
const { entry
|
|
42
|
+
const { entry } = Astro.props;
|
|
46
43
|
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
47
44
|
const siteModel = await getSiteModel(await getCollection('site'));
|
|
45
|
+
const categoryDestinations = siteModel.categoryDestinations.byPathname;
|
|
48
46
|
const { nodes: siteNodes, pages: sitePages } = siteModel;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
?? sitePages.find((page) => page.entry.id === entry?.id);
|
|
52
|
-
if (!currentPage) throw new Error(`Page entry "${entry?.id ?? categoryPath}" is missing from the site page model.`);
|
|
53
|
-
const [siteTheme] = await getCollection('theme');
|
|
54
|
-
if (!siteTheme) {
|
|
55
|
-
throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
|
|
56
|
-
}
|
|
47
|
+
const currentPage = sitePages.find((page) => page.entry.id === entry?.id);
|
|
48
|
+
if (!currentPage) throw new Error(`Page entry "${entry?.id}" is missing from the site page model.`);
|
|
57
49
|
const [sitewide] = await getCollection('sitewide');
|
|
58
50
|
const sitewideData = sitewide?.data ?? {};
|
|
59
51
|
const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
|
|
60
|
-
const siteThemeData = siteTheme.data;
|
|
61
52
|
const pageTheme = await getPageTheme(currentPage.pageDirectory);
|
|
62
|
-
const themeData =
|
|
63
|
-
const themeLabel = pageTheme
|
|
64
|
-
? `${siteDirectory || 'site'}/${pageTheme.id}`
|
|
65
|
-
: `${siteDirectory || 'site'}/${siteTheme.id}`;
|
|
53
|
+
const themeData = pageTheme.data;
|
|
54
|
+
const themeLabel = pageTheme.id;
|
|
66
55
|
const visualTheme = resolveThemeVisualConfig(themeData, themeLabel);
|
|
67
56
|
const logoAsset = getLogoAsset();
|
|
68
57
|
const logoSettings = sitewideData.logo;
|
|
@@ -99,20 +88,20 @@ const renderedNavigationEntries = flattenSiteNavigationTree(listedNavigationTree
|
|
|
99
88
|
const pageSequence = getSequentialPageNavigation(listedNavigationTree, currentPage.pagePath);
|
|
100
89
|
const childPages = getDirectChildPages(listedNavigationTree, currentPage.pagePath);
|
|
101
90
|
const siteHtml = entry?.rendered?.html ?? '';
|
|
102
|
-
const resolvedSections: ResolvedSection[] =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
}
|
|
91
|
+
const resolvedSections: ResolvedSection[] = await getSectionsContent(siteHtml, currentPage, childPages);
|
|
92
|
+
if (currentPage.kind === 'page' && currentPage.entry.data.page?.listChildren) {
|
|
93
|
+
const lastSection = resolvedSections.at(-1);
|
|
94
|
+
if (lastSection && childPages.length > 0) {
|
|
95
|
+
lastSection.contentBlocks.push({
|
|
96
|
+
type: 'page-list',
|
|
97
|
+
pages: childPages.map((child) => ({
|
|
98
|
+
title: child.title,
|
|
99
|
+
pathname: child.pathname,
|
|
100
|
+
description: child.entry.data.page?.description,
|
|
101
|
+
})),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
116
105
|
const allContentBlocks = resolvedSections.flatMap((section) => flattenContentBlocks(section.contentBlocks));
|
|
117
106
|
const hasTabs = allContentBlocks.some((block) => block.type === 'tabs');
|
|
118
107
|
const hasNotes = allContentBlocks.some((block) => (
|
|
@@ -167,33 +156,21 @@ const pagePresentation = resolvePagePresentation(themeData, themeLabel, {
|
|
|
167
156
|
navigationMode: navigationModel.mode,
|
|
168
157
|
});
|
|
169
158
|
const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
|
|
170
|
-
const frameColors = resolveFrameColors({ theme:
|
|
159
|
+
const frameColors = resolveFrameColors({ theme: themeData, sourceLabel: themeLabel });
|
|
171
160
|
const hasTreeNavigation = navigationModel.mode === 'tree';
|
|
172
|
-
|
|
161
|
+
// Keep one reading frame across tree-mode pages, including pages with no H2.
|
|
162
|
+
// Home remains the site's entry point with the global navigation row.
|
|
163
|
+
const areaReadingFrame = hasTreeNavigation && !currentPage.isHome;
|
|
173
164
|
const currentPageNavigation = siteNavigation.find(({ node }) => (
|
|
174
165
|
node.kind === 'page' && node.pathname === currentPage.pathname
|
|
175
166
|
));
|
|
176
167
|
const pageContentsHeadings = currentPageNavigation?.headings.filter(({ depth }) => depth === 2 || depth === 3) ?? [];
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
depth: Math.max(1, node.depth),
|
|
181
|
-
isHome: node.isHome,
|
|
182
|
-
listed: node.navigation.listed,
|
|
183
|
-
pagePath: node.pagePath,
|
|
184
|
-
})),
|
|
185
|
-
currentPage: {
|
|
186
|
-
isHome: currentPage.isHome,
|
|
187
|
-
pagePath: currentPage.pagePath,
|
|
188
|
-
},
|
|
189
|
-
headingCount: pageContentsHeadings.length,
|
|
190
|
-
});
|
|
191
|
-
const hasContentsNavigation = pageContentsPlacement.placement !== 'none';
|
|
192
|
-
const hasTrackedSectionNavigation = hasContentsNavigation
|
|
193
|
-
|| (hasTreeNavigation && currentPage.isHome && pageContentsHeadings.length >= 2);
|
|
168
|
+
const showTreeNavigation = hasTreeNavigation && !currentPage.isHome;
|
|
169
|
+
const hasContentsNavigation = showTreeNavigation && pageContentsHeadings.some(({ depth }) => depth === 2);
|
|
170
|
+
const hasTrackedSectionNavigation = hasTreeNavigation && pageContentsHeadings.some(({ depth }) => depth === 2);
|
|
194
171
|
const integratePageContentsInTree = hasTreeNavigation;
|
|
195
|
-
|
|
196
|
-
const hasNavigationRailFrame =
|
|
172
|
+
// Home's global row and the reading layout share the site's wide coordinates.
|
|
173
|
+
const hasNavigationRailFrame = (
|
|
197
174
|
navigationModel.requestedMode === 'tree'
|
|
198
175
|
|| (
|
|
199
176
|
navigationModel.requestedMode === 'automatic'
|
|
@@ -215,30 +192,35 @@ const reserveBreadcrumbSpace = showTreeNavigation || showBreadcrumbs;
|
|
|
215
192
|
footer={sitewideData.footer}
|
|
216
193
|
contentsNavigation={hasTrackedSectionNavigation}
|
|
217
194
|
searchable
|
|
195
|
+
areaNavigation={hasTreeNavigation}
|
|
196
|
+
areaReadingFrame={areaReadingFrame}
|
|
218
197
|
>
|
|
219
198
|
<SiteNavigation
|
|
220
199
|
logo={logo}
|
|
221
200
|
currentPage={currentPage}
|
|
222
201
|
navigationEntries={siteNavigation}
|
|
202
|
+
categoryDestinations={categoryDestinations}
|
|
223
203
|
navigationMode={navigationModel.mode}
|
|
224
204
|
showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
|
|
225
205
|
hasNavigationRailFrame={hasNavigationRailFrame}
|
|
226
206
|
frameColors={frameColors}
|
|
227
207
|
readerPreferences={pagePresentation.readerPreferences}
|
|
208
|
+
areaReadingFrame={areaReadingFrame}
|
|
228
209
|
/>
|
|
229
210
|
{siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
|
|
230
211
|
|
|
231
212
|
<div class:list={['site-page-layout', {
|
|
232
213
|
'site-page-layout-tree': showTreeNavigation,
|
|
233
|
-
'site-page-layout-contents': showContentsRail,
|
|
234
214
|
}]}
|
|
235
|
-
data-page-contents-placement={hasContentsNavigation ?
|
|
215
|
+
data-page-contents-placement={hasContentsNavigation ? 'page-tree' : undefined}
|
|
236
216
|
>
|
|
237
217
|
{showTreeNavigation && (
|
|
238
218
|
<SiteTreeNavigation
|
|
239
219
|
currentPage={currentPage}
|
|
240
220
|
navigationEntries={siteNavigation}
|
|
221
|
+
categoryDestinations={categoryDestinations}
|
|
241
222
|
integratePageSections={integratePageContentsInTree}
|
|
223
|
+
areaReadingFrame={areaReadingFrame}
|
|
242
224
|
/>
|
|
243
225
|
)}
|
|
244
226
|
<main
|
|
@@ -273,12 +255,6 @@ const reserveBreadcrumbSpace = showTreeNavigation || showBreadcrumbs;
|
|
|
273
255
|
)}
|
|
274
256
|
<PageSequenceNavigation previous={pageSequence.previous} next={pageSequence.next} />
|
|
275
257
|
</main>
|
|
276
|
-
{showContentsRail && (
|
|
277
|
-
<PageContentsNavigation
|
|
278
|
-
headings={pageContentsHeadings}
|
|
279
|
-
pageTitle={currentPage.title}
|
|
280
|
-
/>
|
|
281
|
-
)}
|
|
282
258
|
</div>
|
|
283
259
|
{hasTreeNavigation && <TreeNavigationScript />}
|
|
284
260
|
{hasCodeBlocks && <CodeBlockCopyScript />}
|
|
@@ -1,45 +1,65 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { withBasePath } from '../lib/basePath';
|
|
4
|
-
import { getListedSiteNavigationTree, type SiteNavigationEntry } from '../lib/siteNavigation';
|
|
4
|
+
import { getListedSiteNavigationTree, getNavigationDestination, type CategoryDestinationMap, type SiteNavigationEntry } from '../lib/siteNavigation';
|
|
5
|
+
import { resolveAreaNavigation, withH2Outlines } from '../lib/areaNavigation';
|
|
5
6
|
import type { SiteNode } from '../lib/sitePages';
|
|
6
7
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
7
8
|
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
9
|
+
import NavigationState from './NavigationState.astro';
|
|
10
|
+
import PageContentsLinks from './PageContentsLinks.astro';
|
|
8
11
|
|
|
9
12
|
interface Props {
|
|
10
13
|
currentPage: SiteNode;
|
|
11
14
|
navigationEntries: SiteNavigationEntry[];
|
|
15
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
12
16
|
integratePageSections?: boolean;
|
|
17
|
+
areaReadingFrame?: boolean;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
const { currentPage, navigationEntries, integratePageSections = false } = Astro.props;
|
|
16
|
-
const roots = getListedSiteNavigationTree(navigationEntries);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
))
|
|
21
|
-
|
|
20
|
+
const { currentPage, navigationEntries, categoryDestinations, integratePageSections = false, areaReadingFrame = false } = Astro.props;
|
|
21
|
+
const roots = withH2Outlines(getListedSiteNavigationTree(navigationEntries));
|
|
22
|
+
const area = resolveAreaNavigation(roots, currentPage);
|
|
23
|
+
const activeRoot = area.localRoot;
|
|
24
|
+
const ownOutline = !activeRoot
|
|
25
|
+
? navigationEntries.find(({ node }) => node.pagePath === currentPage.pagePath)?.headings.filter(({ depth }) => depth === 2) ?? []
|
|
26
|
+
: [];
|
|
27
|
+
const navigationPage = activeRoot?.node ?? currentPage;
|
|
28
|
+
const navigationRoot = navigationPage.pagePath ? `/${navigationPage.pagePath}/` : '/';
|
|
22
29
|
const navigationStateRoot = withBasePath(projectConfig.site.basePath, '/');
|
|
30
|
+
const showAreaTitle = activeRoot?.node.kind === 'category';
|
|
31
|
+
const showReadingAreaTitle = areaReadingFrame
|
|
32
|
+
&& navigationPage.pagePath !== area?.globalRoot?.node.pagePath;
|
|
33
|
+
const localNodes = activeRoot ? (showAreaTitle ? activeRoot.children : [activeRoot]) : [];
|
|
34
|
+
// Retain the reviewed state namespace so existing sessions keep their position.
|
|
23
35
|
---
|
|
24
36
|
|
|
25
|
-
{activeRoot && (
|
|
37
|
+
{(activeRoot || ownOutline.length > 0) && (
|
|
26
38
|
<aside
|
|
27
39
|
id="tree-local-navigation"
|
|
28
40
|
class="tree-local-navigation"
|
|
29
|
-
data-navigation-root={navigationRoot}
|
|
30
|
-
data-navigation-state-root={navigationStateRoot}
|
|
41
|
+
data-navigation-root={`area:${navigationRoot}`}
|
|
42
|
+
data-navigation-state-root={`area:${navigationStateRoot}`}
|
|
43
|
+
data-navigation-area={activeRoot?.node.pagePath}
|
|
44
|
+
data-navigation-outline={ownOutline.length > 0 ? currentPage.pagePath : undefined}
|
|
31
45
|
>
|
|
32
|
-
<
|
|
46
|
+
<NavigationState scope="desktop" />
|
|
47
|
+
{showReadingAreaTitle && <p class="navigation-area-title">{navigationPage.title}</p>}
|
|
48
|
+
{!areaReadingFrame && (showAreaTitle || ownOutline.length > 0) && <a class="navigation-area-title" href={withBasePath(projectConfig.site.basePath,
|
|
49
|
+
getNavigationDestination(navigationPage, categoryDestinations).pathname)}>{navigationPage.title}</a>}
|
|
50
|
+
{activeRoot && <NavigationTreeControls
|
|
33
51
|
idPrefix="desktop-tree-navigation"
|
|
34
|
-
nodes={
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
nodes={localNodes}
|
|
53
|
+
forceVisible
|
|
54
|
+
/>}
|
|
55
|
+
<nav aria-label={`${navigationPage.title}: ${projectConfig.locale.labels.siteNavigation}`}>
|
|
56
|
+
{activeRoot ? <NavigationPageTree
|
|
57
|
+
nodes={localNodes}
|
|
39
58
|
currentPage={currentPage}
|
|
59
|
+
categoryDestinations={categoryDestinations}
|
|
40
60
|
variant="sidebar"
|
|
41
61
|
integratePageSections={integratePageSections}
|
|
42
|
-
/>
|
|
62
|
+
/> : <PageContentsLinks headings={ownOutline} variant="desktop" />}
|
|
43
63
|
</nav>
|
|
44
64
|
</aside>
|
|
45
65
|
)}
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
import { ChevronDown } from '@lucide/astro';
|
|
3
3
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
4
|
import { withBasePath } from '../lib/basePath';
|
|
5
|
-
import type { SiteNavigationNode } from '../lib/siteNavigation';
|
|
5
|
+
import type { CategoryDestinationMap, SiteNavigationNode } from '../lib/siteNavigation';
|
|
6
6
|
import type { SiteNode } from '../lib/sitePages';
|
|
7
7
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
10
10
|
node: SiteNavigationNode;
|
|
11
11
|
currentPage?: SiteNode;
|
|
12
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
12
13
|
showSections: boolean;
|
|
13
14
|
showChildren: boolean;
|
|
14
15
|
}
|
|
15
|
-
const { node, currentPage, showSections, showChildren } = Astro.props;
|
|
16
|
+
const { node, currentPage, categoryDestinations, showSections, showChildren } = Astro.props;
|
|
16
17
|
const sections = showSections ? node.sections : [];
|
|
17
18
|
const children = showChildren ? node.children : [];
|
|
18
19
|
const pageHref = node.node.kind === 'page'
|
|
@@ -39,7 +40,7 @@ const label = `${projectConfig.locale.labels.navigationMenu}: ${node.node.title}
|
|
|
39
40
|
{children.length > 0 && (
|
|
40
41
|
<nav class="top-page-children" aria-label={`${projectConfig.locale.labels.navigationChildren}: ${node.node.title}`}>
|
|
41
42
|
{sections.length > 0 && <p>{projectConfig.locale.labels.navigationChildren}</p>}
|
|
42
|
-
<NavigationPageTree nodes={children} currentPage={currentPage} variant="submenu" />
|
|
43
|
+
<NavigationPageTree nodes={children} currentPage={currentPage} categoryDestinations={categoryDestinations} variant="submenu" />
|
|
43
44
|
</nav>
|
|
44
45
|
)}
|
|
45
46
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { createNavigationStateHelpers } from '../lib/navigationState.mjs';
|
|
3
|
+
|
|
2
4
|
type NavigationState = {
|
|
3
5
|
openPaths: string[];
|
|
4
6
|
sectionOpenByPath: Record<string, boolean>;
|
|
@@ -10,12 +12,12 @@
|
|
|
10
12
|
sectionOpen: boolean[];
|
|
11
13
|
scrollTop: number;
|
|
12
14
|
};
|
|
15
|
+
const { storageKeys, parseState, initialState, arrivalPolicy, readEntryState, saveEntryState, pageIsOpen, sectionsAreOpen } = createNavigationStateHelpers();
|
|
13
16
|
|
|
14
17
|
const setupNavigationState = (container: HTMLElement, scope: 'desktop' | 'mobile') => {
|
|
15
18
|
const root = container.dataset.navigationRoot ?? '/';
|
|
16
19
|
const sharedRoot = container.dataset.navigationStateRoot ?? root;
|
|
17
|
-
const stateKey =
|
|
18
|
-
const sharedStateKey = `norna:tree-navigation:shared:${sharedRoot}`;
|
|
20
|
+
const { stateKey, sharedStateKey } = storageKeys(scope, root, sharedRoot);
|
|
19
21
|
const branches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
|
|
20
22
|
'.navigation-page-disclosure[data-page-path]',
|
|
21
23
|
));
|
|
@@ -38,22 +40,7 @@
|
|
|
38
40
|
const scrollContainer = getScrollContainer();
|
|
39
41
|
let filterSnapshot: NavigationSnapshot | null = null;
|
|
40
42
|
let lastSharedState = '';
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const parsedState = JSON.parse(value ?? '{}');
|
|
44
|
-
return {
|
|
45
|
-
openPaths: Array.isArray(parsedState.openPaths) ? parsedState.openPaths : [],
|
|
46
|
-
sectionOpenByPath: parsedState.sectionOpenByPath
|
|
47
|
-
&& typeof parsedState.sectionOpenByPath === 'object'
|
|
48
|
-
? Object.fromEntries(Object.entries(parsedState.sectionOpenByPath)
|
|
49
|
-
.filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean'))
|
|
50
|
-
: {},
|
|
51
|
-
scrollTop: Number.isFinite(Number(parsedState.scrollTop)) ? Number(parsedState.scrollTop) : 0,
|
|
52
|
-
};
|
|
53
|
-
} catch {
|
|
54
|
-
return { openPaths: [], sectionOpenByPath: {}, scrollTop: 0 };
|
|
55
|
-
}
|
|
56
|
-
};
|
|
43
|
+
let preserveOutgoingEntry = false;
|
|
57
44
|
const getDisclosureState = (): SharedNavigationState => ({
|
|
58
45
|
openPaths: branches.filter((branch) => branch.open)
|
|
59
46
|
.map((branch) => branch.dataset.pagePath ?? ''),
|
|
@@ -64,23 +51,13 @@
|
|
|
64
51
|
});
|
|
65
52
|
const applyDisclosureState = (
|
|
66
53
|
state: SharedNavigationState,
|
|
67
|
-
|
|
54
|
+
policy: { onArrival?: boolean; preserveCurrentPage?: boolean; preserveCurrentOutline?: boolean } = {},
|
|
68
55
|
) => {
|
|
69
56
|
branches.forEach((branch) => {
|
|
70
|
-
|
|
71
|
-
branch.open = (openCurrentPageOnArrival && branch.dataset.currentBranch === 'true')
|
|
72
|
-
|| state.openPaths.includes(pagePath);
|
|
57
|
+
branch.open = pageIsOpen(state, branch.dataset, policy);
|
|
73
58
|
});
|
|
74
59
|
sectionBranches.forEach((branch) => {
|
|
75
|
-
|
|
76
|
-
const storedOpen = state.sectionOpenByPath[pagePath];
|
|
77
|
-
if (openCurrentPageOnArrival && branch.dataset.currentPage === 'true') {
|
|
78
|
-
branch.open = true;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
branch.open = typeof storedOpen === 'boolean'
|
|
82
|
-
? storedOpen
|
|
83
|
-
: branch.dataset.currentPage === 'true';
|
|
60
|
+
branch.open = sectionsAreOpen(state, branch.dataset, policy);
|
|
84
61
|
});
|
|
85
62
|
};
|
|
86
63
|
const mergeSharedState = (state: SharedNavigationState): SharedNavigationState => {
|
|
@@ -99,18 +76,16 @@
|
|
|
99
76
|
};
|
|
100
77
|
|
|
101
78
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
applyDisclosureState(
|
|
105
|
-
? storedState
|
|
106
|
-
: parseState(storedSharedState), true);
|
|
79
|
+
const state = initialState(sessionStorage.getItem(stateKey), sessionStorage.getItem(sharedStateKey),
|
|
80
|
+
readEntryState(stateKey));
|
|
81
|
+
applyDisclosureState(state.disclosure, arrivalPolicy(state));
|
|
107
82
|
const initialSharedState = mergeSharedState(getDisclosureState());
|
|
108
83
|
lastSharedState = JSON.stringify(initialSharedState);
|
|
109
84
|
sessionStorage.setItem(sharedStateKey, lastSharedState);
|
|
110
85
|
|
|
111
|
-
if (
|
|
86
|
+
if (state.local.scrollTop > 0 || state.fromEntry) {
|
|
112
87
|
requestAnimationFrame(() => {
|
|
113
|
-
scrollContainer.scrollTop =
|
|
88
|
+
scrollContainer.scrollTop = Math.max(0, state.local.scrollTop);
|
|
114
89
|
});
|
|
115
90
|
}
|
|
116
91
|
} catch {
|
|
@@ -127,6 +102,7 @@
|
|
|
127
102
|
scrollTop: scrollContainer.scrollTop,
|
|
128
103
|
};
|
|
129
104
|
sessionStorage.setItem(stateKey, JSON.stringify(state));
|
|
105
|
+
if (!preserveOutgoingEntry) saveEntryState(stateKey, state);
|
|
130
106
|
const sharedState = mergeSharedState(disclosureState);
|
|
131
107
|
const serializedSharedState = JSON.stringify(sharedState);
|
|
132
108
|
sessionStorage.setItem(sharedStateKey, serializedSharedState);
|
|
@@ -278,23 +254,6 @@
|
|
|
278
254
|
- Math.max(0, (scrollContainer.clientHeight - linkBox.height) / 2);
|
|
279
255
|
setStatus(labels.located);
|
|
280
256
|
};
|
|
281
|
-
const openLinkedPage = (link: HTMLAnchorElement) => {
|
|
282
|
-
if (filterInput?.value) {
|
|
283
|
-
filterInput.value = '';
|
|
284
|
-
clearFilter();
|
|
285
|
-
}
|
|
286
|
-
const pageNode = link.closest<HTMLElement>('.navigation-page-node');
|
|
287
|
-
const pageDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
288
|
-
':scope > .navigation-page-disclosure:not(.navigation-category-disclosure)',
|
|
289
|
-
);
|
|
290
|
-
const sectionDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
291
|
-
':scope > .navigation-page-sections-disclosure[data-page-path]',
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
if (pageDisclosure) pageDisclosure.open = true;
|
|
295
|
-
if (sectionDisclosure) sectionDisclosure.open = true;
|
|
296
|
-
saveState();
|
|
297
|
-
};
|
|
298
257
|
|
|
299
258
|
requestAnimationFrame(() => {
|
|
300
259
|
branches.forEach((branch) => branch.addEventListener('toggle', saveState));
|
|
@@ -314,8 +273,42 @@
|
|
|
314
273
|
collapseButton?.addEventListener('click', () => setAllOpen(false));
|
|
315
274
|
locateButton?.addEventListener('click', locateCurrentPage);
|
|
316
275
|
scrollContainer.addEventListener('scroll', saveState, { passive: true });
|
|
317
|
-
|
|
318
|
-
|
|
276
|
+
// Cross-document arrivals open their current branch in the early
|
|
277
|
+
// restore. Do not expand the departing tree and paint an extra frame.
|
|
278
|
+
document.addEventListener('click', (event) => {
|
|
279
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
280
|
+
const link = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[href]') : null;
|
|
281
|
+
if (!link || link.hasAttribute('download') || (link.target && link.target !== '_self')) return;
|
|
282
|
+
const url = new URL(link.href, location.href);
|
|
283
|
+
if (url.origin !== location.origin || url.pathname !== location.pathname || url.search !== location.search || !currentPageNode) return;
|
|
284
|
+
// A same-document fragment creates a new history entry after this
|
|
285
|
+
// click. Keep the old entry's disclosure snapshot until then.
|
|
286
|
+
preserveOutgoingEntry = url.href !== location.href;
|
|
287
|
+
if (filterInput?.value) { filterInput.value = ''; clearFilter(); }
|
|
288
|
+
const scrollTop = scrollContainer.scrollTop;
|
|
289
|
+
revealNodeAndAncestors(currentPageNode);
|
|
290
|
+
currentPageNode.querySelectorAll<HTMLDetailsElement>(
|
|
291
|
+
':scope > .navigation-page-disclosure, :scope > .navigation-page-sections-disclosure',
|
|
292
|
+
).forEach((branch) => { branch.open = true; });
|
|
293
|
+
scrollContainer.scrollTop = scrollTop;
|
|
294
|
+
saveState();
|
|
295
|
+
});
|
|
296
|
+
const restoreEntry = () => {
|
|
297
|
+
preserveOutgoingEntry = false;
|
|
298
|
+
try {
|
|
299
|
+
const source = readEntryState(stateKey);
|
|
300
|
+
if (source === null) return;
|
|
301
|
+
const state = parseState(source);
|
|
302
|
+
applyDisclosureState(state);
|
|
303
|
+
scrollContainer.scrollTop = Math.max(0, state.scrollTop);
|
|
304
|
+
saveState();
|
|
305
|
+
} catch { /* Native links work when storage is unavailable. */ }
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener('pageshow', (event) => { if (event.persisted) restoreEntry(); });
|
|
308
|
+
window.addEventListener('popstate', restoreEntry);
|
|
309
|
+
window.addEventListener('hashchange', () => { preserveOutgoingEntry = false; saveState(); });
|
|
310
|
+
requestAnimationFrame(saveState);
|
|
311
|
+
|
|
319
312
|
window.addEventListener('pagehide', saveState);
|
|
320
313
|
};
|
|
321
314
|
|