@janga/norna 0.7.24 → 0.7.26
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 +5 -4
- package/astro.config.mjs +27 -11
- package/bin/norna-cli.mjs +12 -0
- package/package.json +26 -5
- package/schemas/card-list.schema.json +121 -0
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +342 -33
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/image-carousel.schema.json +43 -0
- package/schemas/image-stack.schema.json +43 -0
- package/schemas/manifest.json +6 -1
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +237 -262
- package/scripts/check-config.mjs +5 -1
- package/scripts/dev-local.mjs +130 -13
- package/scripts/generate-schemas.mjs +15 -3
- package/scripts/init-site.mjs +2 -0
- package/scripts/lib/category-destinations.mjs +48 -0
- package/scripts/lib/code-fence-metadata.mjs +25 -43
- package/scripts/lib/content-tabs.mjs +155 -0
- package/scripts/lib/details-headings.mjs +58 -0
- package/scripts/lib/docusaurus-migration-audit.mjs +603 -0
- package/scripts/lib/edit-source-link.mjs +53 -0
- package/scripts/lib/editor-language-service.mjs +238 -116
- package/scripts/lib/locale-registry.mjs +597 -0
- package/scripts/lib/locales/az-Latn.mjs +78 -0
- package/scripts/lib/locales/be.mjs +78 -0
- package/scripts/lib/locales/bg.mjs +78 -0
- package/scripts/lib/locales/bs.mjs +78 -0
- package/scripts/lib/locales/ca.mjs +78 -0
- package/scripts/lib/locales/cnr.mjs +78 -0
- package/scripts/lib/locales/cs.mjs +78 -0
- package/scripts/lib/locales/da.mjs +78 -0
- package/scripts/lib/locales/de.mjs +78 -0
- package/scripts/lib/locales/el.mjs +78 -0
- package/scripts/lib/locales/en.mjs +78 -0
- package/scripts/lib/locales/es.mjs +78 -0
- package/scripts/lib/locales/et.mjs +78 -0
- package/scripts/lib/locales/fi.mjs +78 -0
- package/scripts/lib/locales/fil.mjs +78 -0
- package/scripts/lib/locales/fr.mjs +78 -0
- package/scripts/lib/locales/ga.mjs +78 -0
- package/scripts/lib/locales/ha.mjs +78 -0
- package/scripts/lib/locales/hr.mjs +78 -0
- package/scripts/lib/locales/hu.mjs +78 -0
- package/scripts/lib/locales/id.mjs +78 -0
- package/scripts/lib/locales/is.mjs +78 -0
- package/scripts/lib/locales/it.mjs +78 -0
- package/scripts/lib/locales/jv.mjs +78 -0
- package/scripts/lib/locales/lb.mjs +78 -0
- package/scripts/lib/locales/lt.mjs +78 -0
- package/scripts/lib/locales/lv.mjs +78 -0
- package/scripts/lib/locales/mk.mjs +78 -0
- package/scripts/lib/locales/mt.mjs +78 -0
- package/scripts/lib/locales/nb.mjs +78 -0
- package/scripts/lib/locales/nl.mjs +78 -0
- package/scripts/lib/locales/nn.mjs +78 -0
- package/scripts/lib/locales/pcm.mjs +78 -0
- package/scripts/lib/locales/pl.mjs +78 -0
- package/scripts/lib/locales/pt.mjs +78 -0
- package/scripts/lib/locales/ro.mjs +78 -0
- package/scripts/lib/locales/ru.mjs +78 -0
- package/scripts/lib/locales/sk.mjs +78 -0
- package/scripts/lib/locales/sl.mjs +78 -0
- package/scripts/lib/locales/sq.mjs +78 -0
- package/scripts/lib/locales/sr-Cyrl.mjs +78 -0
- package/scripts/lib/locales/sr-Latn.mjs +78 -0
- package/scripts/lib/locales/sv.mjs +78 -0
- package/scripts/lib/locales/sw.mjs +78 -0
- package/scripts/lib/locales/tr.mjs +78 -0
- package/scripts/lib/locales/uk.mjs +78 -0
- package/scripts/lib/locales/vi.mjs +78 -0
- package/scripts/lib/markdown-links.mjs +4 -25
- package/scripts/lib/markdown-notes-render-plugin.mjs +75 -0
- package/scripts/lib/markdown-notes.mjs +165 -0
- package/scripts/lib/navigation-model.mjs +1 -1
- package/scripts/lib/norna-markdown-blocks.mjs +123 -447
- package/scripts/lib/norna-markdown-render-plugin.mjs +4 -0
- package/scripts/lib/page-aliases.mjs +12 -1
- package/scripts/lib/page-markdown.mjs +63 -16
- package/scripts/lib/page-move-plan.mjs +22 -5
- package/scripts/lib/pagefind-translations.mjs +620 -0
- package/scripts/lib/presentation-contract.mjs +6 -1
- package/scripts/lib/presentation.mjs +29 -30
- package/scripts/lib/project-config.mjs +22 -163
- package/scripts/lib/schema-definitions.mjs +13 -12
- package/scripts/lib/schema-editor-metadata.mjs +42 -46
- package/scripts/lib/schema-value-definitions.mjs +7 -8
- package/scripts/lib/semantic-callouts.mjs +4 -2
- package/scripts/lib/site-content.mjs +4 -2
- package/scripts/lib/site-link-graph.mjs +16 -6
- package/scripts/lib/site-navigation-tree.mjs +0 -11
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/sitemap.mjs +5 -3
- package/scripts/lib/structured-blocks.mjs +108 -0
- package/scripts/lib/table-render-plugin.mjs +76 -0
- package/scripts/lib/table-row-headers.mjs +218 -0
- package/scripts/lib/theme-presets.mjs +3 -10
- package/scripts/lib/yaml-config.mjs +2 -6
- package/scripts/migrate-check.mjs +19 -0
- package/scripts/sync-content-sections.mjs +39 -10
- package/scripts/sync-site-public.mjs +7 -1
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +49 -6
- package/src/components/ContentBlocks.astro +39 -0
- package/src/components/ContentTabsScript.astro +60 -0
- package/src/components/DisplaySettings.astro +17 -19
- package/src/components/ImageCarousel.astro +4 -1
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +1 -1
- package/src/components/NoteNavigationScript.astro +4 -0
- package/src/components/PageAliasRedirect.astro +5 -4
- package/src/components/PageContentsNavigation.astro +2 -3
- package/src/components/SearchNavigationScript.astro +4 -0
- package/src/components/SearchPage.astro +43 -2
- package/src/components/SectionNavigationScript.astro +86 -16
- package/src/components/SiteBreadcrumbs.astro +1 -1
- package/src/components/SiteNavigation.astro +98 -65
- package/src/components/SitePage.astro +73 -33
- package/src/components/SiteSection.astro +5 -81
- package/src/components/SiteTreeNavigation.astro +5 -2
- package/src/components/TableOverflowScript.astro +579 -0
- package/src/components/TopPageMenu.astro +47 -0
- package/src/components/TreeNavigationScript.astro +86 -23
- package/src/layouts/BaseLayout.astro +34 -12
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/navigationFollowing.ts +142 -0
- package/src/lib/noteNavigation.ts +50 -0
- package/src/lib/searchNavigation.ts +107 -0
- package/src/lib/sectionContent.ts +23 -116
- package/src/lib/siteNavigation.ts +0 -5
- package/src/lib/sitePages.ts +18 -4
- package/src/lib/table-sort-tooltip.ts +109 -0
- package/src/pages/[...slug].astro +16 -6
- package/src/styles/content.css +563 -26
- package/src/styles/foundations.css +9 -0
- package/src/styles/media.css +192 -3
- package/src/styles/navigation.css +214 -60
- package/src/styles/page-layout.css +55 -16
- package/src/styles/responsive.css +62 -20
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ChevronDown, ChevronsUpDown, ChevronUp } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
|
|
5
|
+
const labels = projectConfig.locale.labels;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<template data-table-navigation-template>
|
|
9
|
+
<div class="norna-table-navigation" data-table-navigation data-table-locale={projectConfig.locale.formattingLocale} hidden>
|
|
10
|
+
<span class="visually-hidden" data-table-overflow-description>{labels.tableOverflowDescription}</span>
|
|
11
|
+
<div class="norna-table-scrollbar" data-table-scrollbar role="scrollbar"
|
|
12
|
+
aria-label={labels.tableColumns} aria-orientation="horizontal"
|
|
13
|
+
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" tabindex="0">
|
|
14
|
+
<span class="norna-table-scrollbar-thumb" data-table-scrollbar-thumb aria-hidden="true"></span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<template data-table-sort-indicator-template
|
|
20
|
+
data-sort-ascending={labels.tableSortAscending}
|
|
21
|
+
data-sort-descending={labels.tableSortDescending}
|
|
22
|
+
data-sort-reset={labels.tableSortReset}>
|
|
23
|
+
<span class="norna-table-sort-indicator" aria-hidden="true">
|
|
24
|
+
<span data-table-sort-icon="unsorted"><ChevronsUpDown size={15} stroke-width={1.8} /></span>
|
|
25
|
+
<span data-table-sort-icon="ascending"><ChevronUp size={15} stroke-width={1.8} /></span>
|
|
26
|
+
<span data-table-sort-icon="descending"><ChevronDown size={15} stroke-width={1.8} /></span>
|
|
27
|
+
</span>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import { createTableSortTooltip } from '../lib/table-sort-tooltip';
|
|
32
|
+
|
|
33
|
+
type TableLayout = 'contained' | 'prose' | 'end' | 'canvas';
|
|
34
|
+
type SortDirection = 'ascending' | 'descending';
|
|
35
|
+
type ScrollbarControl = { element: HTMLElement; thumb: HTMLElement; navigation: HTMLElement };
|
|
36
|
+
const layoutUpdaters = new Set<() => void>();
|
|
37
|
+
let sortTooltip: ReturnType<typeof createTableSortTooltip> | undefined;
|
|
38
|
+
const navigationTemplate = document.querySelector<HTMLTemplateElement>('[data-table-navigation-template]');
|
|
39
|
+
const sortIndicatorTemplate = document.querySelector<HTMLTemplateElement>('[data-table-sort-indicator-template]');
|
|
40
|
+
const sortActions = {
|
|
41
|
+
unsorted: sortIndicatorTemplate?.dataset.sortAscending ?? '',
|
|
42
|
+
ascending: sortIndicatorTemplate?.dataset.sortDescending ?? '',
|
|
43
|
+
descending: sortIndicatorTemplate?.dataset.sortReset ?? '',
|
|
44
|
+
};
|
|
45
|
+
const tableLocale = navigationTemplate?.content.querySelector<HTMLElement>('[data-table-locale]')?.dataset.tableLocale ?? 'en';
|
|
46
|
+
const synchronizedAttributes = [
|
|
47
|
+
'data-appearance',
|
|
48
|
+
'data-focus-reading',
|
|
49
|
+
'data-reading-width',
|
|
50
|
+
'dir',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const initializeTableFrame = (frame: HTMLElement, frameIndex: number) => {
|
|
54
|
+
const scrollRegion = frame.querySelector<HTMLElement>('[data-table-scroll]');
|
|
55
|
+
const table = scrollRegion?.querySelector<HTMLTableElement>('table');
|
|
56
|
+
if (!scrollRegion || frame.dataset.tableReady === 'true') return;
|
|
57
|
+
const isTopLevel = frame.parentElement?.classList.contains('section-markdown') === true;
|
|
58
|
+
const tableHead = table?.tHead;
|
|
59
|
+
const headingCells = Array.from(tableHead?.querySelectorAll<HTMLTableCellElement>('th') ?? []);
|
|
60
|
+
const hasRowHeaders = frame.dataset.tableRowHeaders === 'true';
|
|
61
|
+
let scheduledFrame: number | null = null;
|
|
62
|
+
let stickyHeading: HTMLElement | null = null;
|
|
63
|
+
let stickyHeadingTrack: HTMLElement | null = null;
|
|
64
|
+
const scrollbars: ScrollbarControl[] = [];
|
|
65
|
+
let scrollbarDrag: { control: ScrollbarControl; pointerId: number; grabOffset: number } | null = null;
|
|
66
|
+
let overflowDescriptionId: string | null = null;
|
|
67
|
+
const tableBody = table?.tBodies.length === 1 ? table.tBodies[0] : null;
|
|
68
|
+
const bodyRows = Array.from(tableBody?.rows ?? []);
|
|
69
|
+
const originalRows = bodyRows.map((row, index) => ({ row, index }));
|
|
70
|
+
let sortedColumn: number | null = null;
|
|
71
|
+
let sortDirection: SortDirection | null = null;
|
|
72
|
+
const sortButtons: HTMLButtonElement[] = [];
|
|
73
|
+
const stickyCells: HTMLElement[] = [];
|
|
74
|
+
const stickySortButtons: HTMLButtonElement[] = [];
|
|
75
|
+
// One sticky surface keeps the heading and scrollbar together at both table edges.
|
|
76
|
+
const stickyContext = document.createElement('div');
|
|
77
|
+
stickyContext.className = 'norna-table-sticky-context';
|
|
78
|
+
frame.insertBefore(stickyContext, scrollRegion);
|
|
79
|
+
|
|
80
|
+
const removeDuplicateIdentity = (element: HTMLElement) => {
|
|
81
|
+
element.removeAttribute('id');
|
|
82
|
+
element.querySelectorAll<HTMLElement>('[id]').forEach((descendant) => descendant.removeAttribute('id'));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const createStickyHeading = () => {
|
|
86
|
+
if (!isTopLevel || !tableHead || headingCells.length === 0) return;
|
|
87
|
+
|
|
88
|
+
stickyHeading = document.createElement('div');
|
|
89
|
+
stickyHeading.className = 'norna-table-sticky-heading';
|
|
90
|
+
stickyHeading.dataset.tableStickyHeading = '';
|
|
91
|
+
stickyHeading.setAttribute('aria-hidden', 'true');
|
|
92
|
+
stickyHeading.setAttribute('inert', '');
|
|
93
|
+
|
|
94
|
+
stickyHeadingTrack = document.createElement('div');
|
|
95
|
+
stickyHeadingTrack.className = 'norna-table-sticky-heading-track';
|
|
96
|
+
stickyHeading.append(stickyHeadingTrack);
|
|
97
|
+
stickyContext.prepend(stickyHeading);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const createTableNavigation = () => {
|
|
101
|
+
if (!navigationTemplate) return;
|
|
102
|
+
if (!scrollRegion.id) scrollRegion.id = `norna-table-scroll-${frameIndex + 1}`;
|
|
103
|
+
overflowDescriptionId = `norna-table-overflow-${frameIndex + 1}`;
|
|
104
|
+
for (const placement of ['top', 'bottom']) {
|
|
105
|
+
const fragment = navigationTemplate.content.cloneNode(true) as DocumentFragment;
|
|
106
|
+
const navigation = fragment.querySelector<HTMLElement>('[data-table-navigation]');
|
|
107
|
+
const element = navigation?.querySelector<HTMLElement>('[data-table-scrollbar]');
|
|
108
|
+
const thumb = element?.querySelector<HTMLElement>('[data-table-scrollbar-thumb]');
|
|
109
|
+
if (!navigation || !element || !thumb) continue;
|
|
110
|
+
navigation.dataset.tableNavigation = placement;
|
|
111
|
+
element.setAttribute('aria-controls', scrollRegion.id);
|
|
112
|
+
const description = navigation.querySelector<HTMLElement>('[data-table-overflow-description]');
|
|
113
|
+
if (placement === 'top' && description) description.id = overflowDescriptionId;
|
|
114
|
+
else description?.remove();
|
|
115
|
+
scrollbars.push({ element, thumb, navigation });
|
|
116
|
+
if (placement === 'top') stickyContext.append(fragment);
|
|
117
|
+
else frame.append(fragment);
|
|
118
|
+
}
|
|
119
|
+
if (scrollbars.length === 2) frame.dataset.tableCustomScrollbars = 'true';
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const cellText = (cell: HTMLTableCellElement) => (cell.textContent ?? '').replace(/\s+/gu, ' ').trim();
|
|
123
|
+
|
|
124
|
+
const parseNumber = (value: string) => {
|
|
125
|
+
if (!/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(value)) return null;
|
|
126
|
+
const number = Number(value);
|
|
127
|
+
return Number.isFinite(number) ? number : null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const parseIsoDate = (value: string) => {
|
|
131
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(value);
|
|
132
|
+
if (dateOnly) {
|
|
133
|
+
const year = Number(dateOnly[1]);
|
|
134
|
+
const month = Number(dateOnly[2]);
|
|
135
|
+
const day = Number(dateOnly[3]);
|
|
136
|
+
const timestamp = Date.UTC(year, month - 1, day);
|
|
137
|
+
const date = new Date(timestamp);
|
|
138
|
+
return date.getUTCFullYear() === year
|
|
139
|
+
&& date.getUTCMonth() === month - 1
|
|
140
|
+
&& date.getUTCDate() === day
|
|
141
|
+
? timestamp
|
|
142
|
+
: null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?$/u.test(value)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const timestamp = Date.parse(value);
|
|
150
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getColumnType = (columnIndex: number) => {
|
|
154
|
+
const values = originalRows
|
|
155
|
+
.map(({ row }) => cellText(row.cells[columnIndex]))
|
|
156
|
+
.filter((value) => value !== '');
|
|
157
|
+
if (values.length === 0) return 'text';
|
|
158
|
+
if (values.every((value) => parseNumber(value) !== null)) return 'number';
|
|
159
|
+
if (values.every((value) => parseIsoDate(value) !== null)) return 'date';
|
|
160
|
+
return 'text';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const getCollator = () => {
|
|
164
|
+
try {
|
|
165
|
+
return new Intl.Collator(tableLocale, {
|
|
166
|
+
numeric: false,
|
|
167
|
+
sensitivity: 'variant',
|
|
168
|
+
usage: 'sort',
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
return new Intl.Collator(undefined, {
|
|
172
|
+
numeric: false,
|
|
173
|
+
sensitivity: 'variant',
|
|
174
|
+
usage: 'sort',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isSortableTable = () => {
|
|
180
|
+
if (!tableBody || bodyRows.length === 0 || headingCells.length === 0 || tableHead?.rows.length !== 1) return false;
|
|
181
|
+
const expectedColumns = headingCells.length;
|
|
182
|
+
return headingCells.every((cell) => cell.colSpan === 1 && cell.rowSpan === 1
|
|
183
|
+
&& !cell.querySelector('a, button, input, select, textarea, summary, [tabindex], [contenteditable]'))
|
|
184
|
+
&& bodyRows.every((row) => row.cells.length === expectedColumns
|
|
185
|
+
&& Array.from(row.cells).every((cell) => cell.colSpan === 1 && cell.rowSpan === 1));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const updateSortState = (activeColumn: number | null, direction: SortDirection | null) => {
|
|
189
|
+
headingCells.forEach((headingCell, columnIndex) => {
|
|
190
|
+
headingCell.removeAttribute('aria-sort');
|
|
191
|
+
const button = sortButtons[columnIndex];
|
|
192
|
+
if (!button) return;
|
|
193
|
+
const state = columnIndex === activeColumn && direction ? direction : 'unsorted';
|
|
194
|
+
const action = sortActions[state];
|
|
195
|
+
const columnName = headingCell.getAttribute('aria-label') ?? cellText(headingCell);
|
|
196
|
+
for (const control of [button, stickySortButtons[columnIndex]]) {
|
|
197
|
+
if (!control) continue;
|
|
198
|
+
control.dataset.tableSortState = state;
|
|
199
|
+
control.dataset.tableSortAction = action;
|
|
200
|
+
control.setAttribute('aria-label', `${columnName}: ${action}`);
|
|
201
|
+
sortTooltip?.refresh(control);
|
|
202
|
+
}
|
|
203
|
+
if (columnIndex === activeColumn && direction) headingCell.setAttribute('aria-sort', direction);
|
|
204
|
+
});
|
|
205
|
+
if (activeColumn === null || !direction) {
|
|
206
|
+
table?.removeAttribute('data-table-sort-column');
|
|
207
|
+
table?.removeAttribute('data-table-sort-direction');
|
|
208
|
+
} else {
|
|
209
|
+
table?.setAttribute('data-table-sort-column', String(activeColumn));
|
|
210
|
+
table?.setAttribute('data-table-sort-direction', direction);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const sortRows = (columnIndex: number, direction: SortDirection) => {
|
|
215
|
+
if (!tableBody) return;
|
|
216
|
+
const type = getColumnType(columnIndex);
|
|
217
|
+
const collator = type === 'text' ? getCollator() : null;
|
|
218
|
+
const sortedRows = [...originalRows].sort((left, right) => {
|
|
219
|
+
const leftValue = cellText(left.row.cells[columnIndex]);
|
|
220
|
+
const rightValue = cellText(right.row.cells[columnIndex]);
|
|
221
|
+
if (leftValue === '' || rightValue === '') {
|
|
222
|
+
if (leftValue === rightValue) return left.index - right.index;
|
|
223
|
+
return leftValue === '' ? 1 : -1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let result = 0;
|
|
227
|
+
if (type === 'number') {
|
|
228
|
+
result = (parseNumber(leftValue) ?? 0) - (parseNumber(rightValue) ?? 0);
|
|
229
|
+
} else if (type === 'date') {
|
|
230
|
+
result = (parseIsoDate(leftValue) ?? 0) - (parseIsoDate(rightValue) ?? 0);
|
|
231
|
+
} else {
|
|
232
|
+
result = collator?.compare(leftValue, rightValue) ?? 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result === 0
|
|
236
|
+
? left.index - right.index
|
|
237
|
+
: result * (direction === 'ascending' ? 1 : -1);
|
|
238
|
+
});
|
|
239
|
+
sortedRows.forEach(({ row }) => tableBody.append(row));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const resetRows = () => {
|
|
243
|
+
if (!tableBody) return;
|
|
244
|
+
originalRows.forEach(({ row }) => tableBody.append(row));
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const createTableSorting = () => {
|
|
248
|
+
if (!table || !sortIndicatorTemplate || !isSortableTable()) return;
|
|
249
|
+
sortTooltip ??= createTableSortTooltip();
|
|
250
|
+
headingCells.forEach((headingCell, columnIndex) => {
|
|
251
|
+
const button = document.createElement('button');
|
|
252
|
+
button.type = 'button';
|
|
253
|
+
button.className = 'norna-table-sort-button';
|
|
254
|
+
button.dataset.tableSortButton = '';
|
|
255
|
+
button.dataset.tableSortState = 'unsorted';
|
|
256
|
+
// Keep the semantic column name when its original control is hidden.
|
|
257
|
+
if (!headingCell.hasAttribute('aria-label')) headingCell.setAttribute('aria-label', cellText(headingCell));
|
|
258
|
+
button.append(...Array.from(headingCell.childNodes));
|
|
259
|
+
button.append(sortIndicatorTemplate.content.cloneNode(true));
|
|
260
|
+
headingCell.replaceChildren(button);
|
|
261
|
+
sortButtons.push(button);
|
|
262
|
+
sortTooltip.attach(button);
|
|
263
|
+
button.addEventListener('click', () => {
|
|
264
|
+
if (sortedColumn !== columnIndex) {
|
|
265
|
+
sortedColumn = columnIndex;
|
|
266
|
+
sortDirection = 'ascending';
|
|
267
|
+
sortRows(columnIndex, sortDirection);
|
|
268
|
+
} else if (sortDirection === 'ascending') {
|
|
269
|
+
sortDirection = 'descending';
|
|
270
|
+
sortRows(columnIndex, sortDirection);
|
|
271
|
+
} else {
|
|
272
|
+
resetRows();
|
|
273
|
+
sortedColumn = null;
|
|
274
|
+
sortDirection = null;
|
|
275
|
+
}
|
|
276
|
+
updateSortState(sortedColumn, sortDirection);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
updateSortState(null, null);
|
|
280
|
+
table.dataset.tableSortable = 'true';
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const setOverflowDescription = (enabled: boolean) => {
|
|
284
|
+
if (!overflowDescriptionId) return;
|
|
285
|
+
const descriptionIds = new Set((scrollRegion.getAttribute('aria-describedby') ?? '')
|
|
286
|
+
.split(/\s+/u)
|
|
287
|
+
.filter(Boolean));
|
|
288
|
+
if (enabled) descriptionIds.add(overflowDescriptionId);
|
|
289
|
+
else descriptionIds.delete(overflowDescriptionId);
|
|
290
|
+
if (descriptionIds.size > 0) scrollRegion.setAttribute('aria-describedby', [...descriptionIds].join(' '));
|
|
291
|
+
else scrollRegion.removeAttribute('aria-describedby');
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const getScrollbarGeometry = (control = scrollbars[0]) => {
|
|
295
|
+
const maximum = Math.max(0, scrollRegion.scrollWidth - scrollRegion.clientWidth);
|
|
296
|
+
const rtl = getComputedStyle(scrollRegion).direction === 'rtl';
|
|
297
|
+
const position = Math.min(maximum, Math.max(0, scrollRegion.scrollLeft * (rtl ? -1 : 1)));
|
|
298
|
+
const width = control?.element.getBoundingClientRect().width ?? 0;
|
|
299
|
+
const fraction = scrollRegion.clientWidth / Math.max(1, scrollRegion.scrollWidth);
|
|
300
|
+
const thumbWidth = Math.min(width, Math.max(24, width * fraction));
|
|
301
|
+
return { maximum, rtl, position, width, thumbWidth, travel: width - thumbWidth };
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const setScrollbarPosition = (position: number) => {
|
|
305
|
+
const { maximum, rtl } = getScrollbarGeometry();
|
|
306
|
+
scrollRegion.scrollTo({ left: Math.min(maximum, Math.max(0, position)) * (rtl ? -1 : 1), behavior: 'instant' });
|
|
307
|
+
updateOverflow();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const synchronizeScrollbar = () => {
|
|
311
|
+
for (const control of scrollbars) {
|
|
312
|
+
const { maximum, rtl, position, thumbWidth, travel } = getScrollbarGeometry(control);
|
|
313
|
+
const progress = maximum > 0 ? position / maximum : 0;
|
|
314
|
+
control.thumb.style.width = `${thumbWidth}px`;
|
|
315
|
+
control.thumb.style.left = `${travel * (rtl ? 1 - progress : progress)}px`;
|
|
316
|
+
control.element.setAttribute('aria-valuenow', String(Math.round(progress * 100)));
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const dragScrollbar = (clientX: number) => {
|
|
321
|
+
if (!scrollbarDrag) return;
|
|
322
|
+
const { control } = scrollbarDrag;
|
|
323
|
+
const { maximum, rtl, travel } = getScrollbarGeometry(control);
|
|
324
|
+
if (travel <= 0) return;
|
|
325
|
+
const offset = clientX - control.element.getBoundingClientRect().left - scrollbarDrag.grabOffset;
|
|
326
|
+
const fraction = Math.min(1, Math.max(0, offset / travel));
|
|
327
|
+
setScrollbarPosition(maximum * (rtl ? 1 - fraction : fraction));
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const synchronizeStickyHeadingPosition = () => {
|
|
331
|
+
if (!stickyHeading || !stickyHeadingTrack || !table) return;
|
|
332
|
+
const layerBounds = stickyHeading.getBoundingClientRect();
|
|
333
|
+
const tableBounds = table.getBoundingClientRect();
|
|
334
|
+
const trackOffset = tableBounds.left - layerBounds.left;
|
|
335
|
+
stickyHeadingTrack.style.transform = `translateX(${trackOffset}px)`;
|
|
336
|
+
|
|
337
|
+
const rowHeader = stickyHeadingTrack.querySelector<HTMLElement>('[data-table-sticky-row-header]');
|
|
338
|
+
if (!rowHeader) return;
|
|
339
|
+
const rowHeaderLeft = Number.parseFloat(rowHeader.style.left) || 0;
|
|
340
|
+
const rowHeaderWidth = Number.parseFloat(rowHeader.style.width) || 0;
|
|
341
|
+
const isRightToLeft = getComputedStyle(table).direction === 'rtl';
|
|
342
|
+
const naturalLeft = tableBounds.left + rowHeaderLeft;
|
|
343
|
+
const desiredLeft = isRightToLeft
|
|
344
|
+
? layerBounds.right - rowHeaderWidth
|
|
345
|
+
: layerBounds.left;
|
|
346
|
+
rowHeader.style.transform = `translateX(${desiredLeft - naturalLeft}px)`;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const synchronizeSortControls = (useSticky: boolean) => {
|
|
350
|
+
if (!stickyHeading || sortButtons.length === 0) return;
|
|
351
|
+
const active = document.activeElement;
|
|
352
|
+
const from = useSticky ? sortButtons : stickySortButtons;
|
|
353
|
+
const to = useSticky ? stickySortButtons : sortButtons;
|
|
354
|
+
const focusedColumn = from.findIndex((button) => button === active);
|
|
355
|
+
stickyHeading.toggleAttribute('inert', !useSticky);
|
|
356
|
+
if (useSticky) {
|
|
357
|
+
stickyHeading.removeAttribute('aria-hidden');
|
|
358
|
+
stickyHeading.setAttribute('role', 'group');
|
|
359
|
+
const groupLabel = scrollbars[0]?.element.getAttribute('aria-label');
|
|
360
|
+
if (groupLabel) stickyHeading.setAttribute('aria-label', groupLabel);
|
|
361
|
+
}
|
|
362
|
+
for (const [index, button] of sortButtons.entries()) {
|
|
363
|
+
button.tabIndex = useSticky ? -1 : 0;
|
|
364
|
+
if (!useSticky) button.removeAttribute('aria-hidden');
|
|
365
|
+
if (stickySortButtons[index]) stickySortButtons[index].tabIndex = useSticky ? 0 : -1;
|
|
366
|
+
}
|
|
367
|
+
// Transfer focus before hiding the control that previously owned it.
|
|
368
|
+
if (focusedColumn >= 0) to[focusedColumn]?.focus({ preventScroll: true });
|
|
369
|
+
if (useSticky) sortButtons.forEach((button) => button.setAttribute('aria-hidden', 'true'));
|
|
370
|
+
else stickyHeading.setAttribute('aria-hidden', 'true');
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const synchronizeStickyHeadingGeometry = () => {
|
|
374
|
+
if (!stickyHeading || !stickyHeadingTrack || !table || !tableHead) return false;
|
|
375
|
+
const tableBounds = table.getBoundingClientRect();
|
|
376
|
+
const headingBounds = tableHead.getBoundingClientRect();
|
|
377
|
+
if (tableBounds.width <= 0 || headingBounds.height <= 0) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
frame.style.setProperty('--table-sticky-heading-height', `${headingBounds.height}px`);
|
|
382
|
+
stickyHeadingTrack.style.width = `${tableBounds.width}px`;
|
|
383
|
+
stickyHeadingTrack.style.height = `${headingBounds.height}px`;
|
|
384
|
+
headingCells.forEach((headingCell, columnIndex) => {
|
|
385
|
+
const cellBounds = headingCell.getBoundingClientRect();
|
|
386
|
+
const computedStyle = getComputedStyle(headingCell);
|
|
387
|
+
let visualCell = stickyCells[columnIndex];
|
|
388
|
+
if (!visualCell) {
|
|
389
|
+
visualCell = document.createElement('div');
|
|
390
|
+
visualCell.append(...Array.from(headingCell.childNodes, (node) => node.cloneNode(true)));
|
|
391
|
+
removeDuplicateIdentity(visualCell);
|
|
392
|
+
const button = visualCell.querySelector<HTMLButtonElement>('[data-table-sort-button]');
|
|
393
|
+
if (button) {
|
|
394
|
+
button.removeAttribute('aria-hidden');
|
|
395
|
+
button.tabIndex = -1;
|
|
396
|
+
sortTooltip?.attach(button);
|
|
397
|
+
button.addEventListener('click', () => sortButtons[columnIndex].click());
|
|
398
|
+
// Focus on a clipped column must reveal it in the real scroll region.
|
|
399
|
+
button.addEventListener('focus', () => {
|
|
400
|
+
if (!stickyHeading) return;
|
|
401
|
+
const bounds = visualCell.getBoundingClientRect();
|
|
402
|
+
const viewport = stickyHeading.getBoundingClientRect();
|
|
403
|
+
const pinnedWidth = hasRowHeaders && columnIndex > 0
|
|
404
|
+
? stickyCells[0].getBoundingClientRect().width : 0;
|
|
405
|
+
const rtl = getComputedStyle(scrollRegion).direction === 'rtl';
|
|
406
|
+
const start = viewport.left + (rtl ? 0 : pinnedWidth);
|
|
407
|
+
const end = viewport.right - (rtl ? pinnedWidth : 0);
|
|
408
|
+
const delta = bounds.left < start ? bounds.left - start
|
|
409
|
+
: bounds.right > end ? Math.min(bounds.right - end, bounds.left - start) : 0;
|
|
410
|
+
if (Math.abs(delta) > 1) scrollRegion.scrollLeft += delta;
|
|
411
|
+
synchronizeStickyHeadingPosition();
|
|
412
|
+
});
|
|
413
|
+
stickySortButtons[columnIndex] = button;
|
|
414
|
+
}
|
|
415
|
+
stickyCells[columnIndex] = visualCell;
|
|
416
|
+
stickyHeadingTrack.append(visualCell);
|
|
417
|
+
}
|
|
418
|
+
visualCell.className = 'norna-table-sticky-heading-cell';
|
|
419
|
+
visualCell.style.left = `${cellBounds.left - tableBounds.left}px`;
|
|
420
|
+
visualCell.style.top = `${cellBounds.top - headingBounds.top}px`;
|
|
421
|
+
visualCell.style.width = `${cellBounds.width}px`;
|
|
422
|
+
visualCell.style.height = `${cellBounds.height}px`;
|
|
423
|
+
visualCell.style.font = computedStyle.font;
|
|
424
|
+
visualCell.style.fontWeight = computedStyle.fontWeight;
|
|
425
|
+
visualCell.style.letterSpacing = computedStyle.letterSpacing;
|
|
426
|
+
visualCell.style.lineHeight = computedStyle.lineHeight;
|
|
427
|
+
visualCell.style.padding = computedStyle.padding;
|
|
428
|
+
visualCell.style.textAlign = computedStyle.textAlign;
|
|
429
|
+
visualCell.style.textTransform = computedStyle.textTransform;
|
|
430
|
+
visualCell.style.whiteSpace = computedStyle.whiteSpace;
|
|
431
|
+
visualCell.style.wordBreak = computedStyle.wordBreak;
|
|
432
|
+
visualCell.style.overflowWrap = computedStyle.overflowWrap;
|
|
433
|
+
if (hasRowHeaders && columnIndex === 0) {
|
|
434
|
+
visualCell.classList.add('norna-table-sticky-heading-cell-row-header');
|
|
435
|
+
visualCell.dataset.tableStickyRowHeader = '';
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
synchronizeStickyHeadingPosition();
|
|
439
|
+
return true;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
createTableSorting();
|
|
443
|
+
createTableNavigation();
|
|
444
|
+
createStickyHeading();
|
|
445
|
+
const stopScrollbarDrag = () => {
|
|
446
|
+
const pointerId = scrollbarDrag?.pointerId;
|
|
447
|
+
const scrollbar = scrollbarDrag?.control.element;
|
|
448
|
+
scrollbarDrag = null;
|
|
449
|
+
if (pointerId !== undefined && scrollbar?.hasPointerCapture(pointerId)) scrollbar.releasePointerCapture(pointerId);
|
|
450
|
+
if (scrollbar) delete scrollbar.dataset.dragging;
|
|
451
|
+
};
|
|
452
|
+
for (const control of scrollbars) {
|
|
453
|
+
const { element: scrollbar, thumb: scrollbarThumb } = control;
|
|
454
|
+
scrollbar.addEventListener('keydown', (event) => {
|
|
455
|
+
const { maximum, rtl, position } = getScrollbarGeometry(control);
|
|
456
|
+
const step = Math.max(1, scrollRegion.clientWidth * 0.1);
|
|
457
|
+
const page = scrollRegion.clientWidth * 0.8;
|
|
458
|
+
let next: number;
|
|
459
|
+
switch (event.key) {
|
|
460
|
+
case 'ArrowLeft': next = position + step * (rtl ? 1 : -1); break;
|
|
461
|
+
case 'ArrowRight': next = position + step * (rtl ? -1 : 1); break;
|
|
462
|
+
case 'PageUp': next = position - page; break;
|
|
463
|
+
case 'PageDown': next = position + page; break;
|
|
464
|
+
case 'Home': next = 0; break;
|
|
465
|
+
case 'End': next = maximum; break;
|
|
466
|
+
default: return;
|
|
467
|
+
}
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
setScrollbarPosition(next);
|
|
470
|
+
});
|
|
471
|
+
scrollbar.addEventListener('pointerdown', (event) => {
|
|
472
|
+
if (!event.isPrimary || event.button !== 0) return;
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
scrollbar.focus({ preventScroll: true });
|
|
475
|
+
const bounds = scrollbarThumb.getBoundingClientRect();
|
|
476
|
+
const onThumb = event.clientX >= bounds.left && event.clientX <= bounds.right;
|
|
477
|
+
scrollbarDrag = { control, pointerId: event.pointerId, grabOffset: onThumb ? event.clientX - bounds.left : bounds.width / 2 };
|
|
478
|
+
scrollbar.setPointerCapture(event.pointerId);
|
|
479
|
+
scrollbar.dataset.dragging = 'true';
|
|
480
|
+
dragScrollbar(event.clientX);
|
|
481
|
+
});
|
|
482
|
+
scrollbar.addEventListener('pointermove', (event) => {
|
|
483
|
+
if (scrollbarDrag?.pointerId === event.pointerId) dragScrollbar(event.clientX);
|
|
484
|
+
});
|
|
485
|
+
scrollbar.addEventListener('pointerup', stopScrollbarDrag);
|
|
486
|
+
scrollbar.addEventListener('pointercancel', stopScrollbarDrag);
|
|
487
|
+
scrollbar.addEventListener('lostpointercapture', stopScrollbarDrag);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const updateOverflow = ({ refreshStickyHeading = false } = {}) => {
|
|
491
|
+
const maximumScroll = Math.max(0, scrollRegion.scrollWidth - scrollRegion.clientWidth);
|
|
492
|
+
const hasOverflow = maximumScroll > 1;
|
|
493
|
+
const scrollBounds = scrollRegion.getBoundingClientRect();
|
|
494
|
+
const tableBounds = table?.getBoundingClientRect();
|
|
495
|
+
const isRightToLeft = getComputedStyle(scrollRegion).direction === 'rtl';
|
|
496
|
+
const isAtStart = !hasOverflow || !tableBounds || (isRightToLeft
|
|
497
|
+
? tableBounds.right <= scrollBounds.right + 2
|
|
498
|
+
: tableBounds.left >= scrollBounds.left - 2);
|
|
499
|
+
const isAtEnd = !hasOverflow || !tableBounds || (isRightToLeft
|
|
500
|
+
? tableBounds.left >= scrollBounds.left - 2
|
|
501
|
+
: tableBounds.right <= scrollBounds.right + 2);
|
|
502
|
+
|
|
503
|
+
for (const { navigation } of scrollbars) {
|
|
504
|
+
if (!hasOverflow && navigation.contains(document.activeElement)) {
|
|
505
|
+
scrollRegion.tabIndex = -1;
|
|
506
|
+
scrollRegion.focus({ preventScroll: true });
|
|
507
|
+
}
|
|
508
|
+
if (!hasOverflow) stopScrollbarDrag();
|
|
509
|
+
navigation.hidden = !hasOverflow;
|
|
510
|
+
}
|
|
511
|
+
const stickyHeadingReady = hasOverflow && stickyHeading
|
|
512
|
+
? (refreshStickyHeading
|
|
513
|
+
? synchronizeStickyHeadingGeometry()
|
|
514
|
+
: (stickyHeadingTrack?.childElementCount ?? 0) > 0)
|
|
515
|
+
: false;
|
|
516
|
+
|
|
517
|
+
frame.dataset.tableOverflow = hasOverflow ? 'true' : 'false';
|
|
518
|
+
frame.dataset.tableAtStart = isAtStart ? 'true' : 'false';
|
|
519
|
+
frame.dataset.tableAtEnd = isAtEnd ? 'true' : 'false';
|
|
520
|
+
frame.dataset.tableStickyHeading = stickyHeadingReady ? 'true' : 'false';
|
|
521
|
+
synchronizeSortControls(Boolean(stickyHeadingReady));
|
|
522
|
+
if (hasOverflow) synchronizeScrollbar();
|
|
523
|
+
setOverflowDescription(hasOverflow);
|
|
524
|
+
if (hasOverflow) scrollRegion.setAttribute('tabindex', '0');
|
|
525
|
+
else if (document.activeElement !== scrollRegion) scrollRegion.removeAttribute('tabindex');
|
|
526
|
+
if (stickyHeadingReady && !refreshStickyHeading) synchronizeStickyHeadingPosition();
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const tableFitsLayout = (layout: TableLayout) => {
|
|
530
|
+
frame.dataset.tableLayout = layout;
|
|
531
|
+
return scrollRegion.scrollWidth - scrollRegion.clientWidth <= 1;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const updateLayout = () => {
|
|
535
|
+
scheduledFrame = null;
|
|
536
|
+
if (!isTopLevel || !table) {
|
|
537
|
+
frame.dataset.tableLayout = 'contained';
|
|
538
|
+
updateOverflow();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const candidates: TableLayout[] = ['prose', 'end', 'canvas'];
|
|
543
|
+
const selected = candidates.find(tableFitsLayout) ?? 'canvas';
|
|
544
|
+
frame.dataset.tableLayout = selected;
|
|
545
|
+
updateOverflow({ refreshStickyHeading: true });
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const scheduleLayoutUpdate = () => {
|
|
549
|
+
if (scheduledFrame !== null) return;
|
|
550
|
+
scheduledFrame = requestAnimationFrame(updateLayout);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
frame.dataset.tableReady = 'true';
|
|
554
|
+
scrollRegion.addEventListener('scroll', () => updateOverflow(), { passive: true });
|
|
555
|
+
layoutUpdaters.add(scheduleLayoutUpdate);
|
|
556
|
+
if ('ResizeObserver' in window) {
|
|
557
|
+
const observer = new ResizeObserver(scheduleLayoutUpdate);
|
|
558
|
+
observer.observe(frame);
|
|
559
|
+
observer.observe(scrollRegion);
|
|
560
|
+
if (table) observer.observe(table);
|
|
561
|
+
if (frame.parentElement) observer.observe(frame.parentElement);
|
|
562
|
+
} else {
|
|
563
|
+
window.addEventListener('resize', scheduleLayoutUpdate, { passive: true });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
updateLayout();
|
|
567
|
+
document.fonts?.ready.then(scheduleLayoutUpdate).catch(() => {});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const frames = Array.from(document.querySelectorAll<HTMLElement>('[data-table-frame]'));
|
|
571
|
+
frames.forEach(initializeTableFrame);
|
|
572
|
+
if (frames.length > 0) {
|
|
573
|
+
const preferenceObserver = new MutationObserver(() => layoutUpdaters.forEach((update) => update()));
|
|
574
|
+
preferenceObserver.observe(document.documentElement, {
|
|
575
|
+
attributeFilter: synchronizedAttributes,
|
|
576
|
+
attributes: true,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ChevronDown } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
import { withBasePath } from '../lib/basePath';
|
|
5
|
+
import type { SiteNavigationNode } from '../lib/siteNavigation';
|
|
6
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
7
|
+
import NavigationPageTree from './NavigationPageTree.astro';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
node: SiteNavigationNode;
|
|
11
|
+
currentPage?: SiteNode;
|
|
12
|
+
showSections: boolean;
|
|
13
|
+
showChildren: boolean;
|
|
14
|
+
}
|
|
15
|
+
const { node, currentPage, showSections, showChildren } = Astro.props;
|
|
16
|
+
const sections = showSections ? node.sections : [];
|
|
17
|
+
const children = showChildren ? node.children : [];
|
|
18
|
+
const pageHref = node.node.kind === 'page'
|
|
19
|
+
? withBasePath(projectConfig.site.basePath, node.node.pathname)
|
|
20
|
+
: '';
|
|
21
|
+
const label = `${projectConfig.locale.labels.navigationMenu}: ${node.node.title}`;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
{(sections.length > 0 || children.length > 0) && (
|
|
25
|
+
<details class="top-page-menu">
|
|
26
|
+
<summary aria-label={label} title={label}>
|
|
27
|
+
<ChevronDown aria-hidden="true" />
|
|
28
|
+
</summary>
|
|
29
|
+
<div class="top-page-panel">
|
|
30
|
+
{sections.length > 0 && (
|
|
31
|
+
<nav aria-label={`${projectConfig.locale.labels.pageSections}: ${node.node.title}`}>
|
|
32
|
+
<ul class="top-page-sections">
|
|
33
|
+
{sections.map((section) => (
|
|
34
|
+
<li><a href={`${pageHref}#${section.id}`}>{section.title}</a></li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
</nav>
|
|
38
|
+
)}
|
|
39
|
+
{children.length > 0 && (
|
|
40
|
+
<nav class="top-page-children" aria-label={`${projectConfig.locale.labels.navigationChildren}: ${node.node.title}`}>
|
|
41
|
+
{sections.length > 0 && <p>{projectConfig.locale.labels.navigationChildren}</p>}
|
|
42
|
+
<NavigationPageTree nodes={children} currentPage={currentPage} variant="submenu" />
|
|
43
|
+
</nav>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</details>
|
|
47
|
+
)}
|