@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,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
auditDocusaurusProject,
|
|
3
|
+
migrationCheckUsage,
|
|
4
|
+
parseMigrationCheckArgs,
|
|
5
|
+
writeMigrationReport,
|
|
6
|
+
} from './lib/docusaurus-migration-audit.mjs';
|
|
7
|
+
|
|
8
|
+
const parsed = parseMigrationCheckArgs(process.argv.slice(2));
|
|
9
|
+
if (parsed.help) {
|
|
10
|
+
console.log(migrationCheckUsage);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const report = await auditDocusaurusProject({ sourceRoot: parsed.sourceRoot });
|
|
15
|
+
await writeMigrationReport(report, parsed.reportDir, { sourceRoot: parsed.sourceRoot });
|
|
16
|
+
console.log('Docusaurus migration audit written to ' + parsed.reportDir);
|
|
17
|
+
console.log('Source pages: ' + report.summary.pages);
|
|
18
|
+
console.log('Pages requiring attention: ' + report.summary.pagesWithFindings);
|
|
19
|
+
console.log('Findings: ' + report.summary.findings);
|
|
@@ -233,6 +233,7 @@ try {
|
|
|
233
233
|
const siteStructure = await getSiteStructure();
|
|
234
234
|
const contentFiles = siteStructure.contentFiles;
|
|
235
235
|
for (const warning of siteStructure.warnings) {
|
|
236
|
+
if (warning.code === 'empty-category') continue;
|
|
236
237
|
addIssue({
|
|
237
238
|
severity: 'warning',
|
|
238
239
|
message: warning.message,
|
|
@@ -272,6 +273,14 @@ for (const contentFile of contentFiles) {
|
|
|
272
273
|
prelude,
|
|
273
274
|
regions: sections,
|
|
274
275
|
} = page;
|
|
276
|
+
for (const issue of page.diagnostics.filter((candidate) => candidate.code === 'heading-inside-details')) {
|
|
277
|
+
addContentIssue(contentFile, issue);
|
|
278
|
+
}
|
|
279
|
+
for (const issue of page.noteDiagnostics) {
|
|
280
|
+
const section = sections.find((candidate) => issue.offset >= candidate.startOffset && issue.offset < candidate.endOffset);
|
|
281
|
+
if (section) addSectionIssue(contentFile, section, issue);
|
|
282
|
+
else addContentIssue(contentFile, issue);
|
|
283
|
+
}
|
|
275
284
|
const invalidHeadingLines = new Set();
|
|
276
285
|
for (const issue of headingIdentifierIssues) {
|
|
277
286
|
invalidHeadingLines.add(issue.heading.line);
|
|
@@ -362,14 +371,19 @@ for (const contentFile of contentFiles) {
|
|
|
362
371
|
const blockResults = { blocks: section.blocks, errors: section.blockErrors };
|
|
363
372
|
blockResultsBySection.set(section, blockResults);
|
|
364
373
|
|
|
365
|
-
for (const error of section.
|
|
374
|
+
for (const error of section.tabErrors) {
|
|
375
|
+
addSectionIssue(contentFile, section, { severity: 'error', message: error.message });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const error of section.calloutErrors) {
|
|
366
379
|
addSectionIssue(contentFile, section, {
|
|
367
|
-
severity: 'error',
|
|
380
|
+
severity: error.severity ?? 'error',
|
|
368
381
|
message: error.message,
|
|
382
|
+
fix: error.fix,
|
|
369
383
|
});
|
|
370
384
|
}
|
|
371
385
|
|
|
372
|
-
for (const error of section.
|
|
386
|
+
for (const error of section.codeFenceErrors) {
|
|
373
387
|
addSectionIssue(contentFile, section, {
|
|
374
388
|
severity: 'error',
|
|
375
389
|
message: error.message,
|
|
@@ -377,7 +391,7 @@ for (const contentFile of contentFiles) {
|
|
|
377
391
|
});
|
|
378
392
|
}
|
|
379
393
|
|
|
380
|
-
for (const error of section.
|
|
394
|
+
for (const error of section.tableErrors) {
|
|
381
395
|
addSectionIssue(contentFile, section, {
|
|
382
396
|
severity: 'error',
|
|
383
397
|
message: error.message,
|
|
@@ -421,17 +435,32 @@ const listedNavigationTree = getListedSiteNavigationTree(siteStructure.nodes.map
|
|
|
421
435
|
|
|
422
436
|
for (const context of contentFileContexts) {
|
|
423
437
|
const childPages = getDirectChildPages(listedNavigationTree, context.contentFile.pagePath);
|
|
424
|
-
|
|
438
|
+
const warnedChildren = new Set();
|
|
425
439
|
|
|
426
440
|
for (const section of context.sections) {
|
|
427
441
|
if (!context.validSections.has(section)) continue;
|
|
428
442
|
for (const block of section.blocks) {
|
|
429
443
|
if (block.type !== 'page-list') continue;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
444
|
+
if (childPages.length === 0) {
|
|
445
|
+
addSectionIssue(context.contentFile, section, {
|
|
446
|
+
severity: 'error',
|
|
447
|
+
message: `page-list on line ${block.line} has no listed direct child pages to display. Navigation categories are not pages.`,
|
|
448
|
+
fix: 'Add a listed direct child page or remove the block.',
|
|
449
|
+
});
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
for (const child of childPages) {
|
|
453
|
+
const childContext = contentContextByPageDirectory.get(child.pageDirectory);
|
|
454
|
+
if (!childContext || warnedChildren.has(child.pageDirectory)) continue;
|
|
455
|
+
const description = childContext.frontmatterData.page?.description;
|
|
456
|
+
if (typeof description === 'string' && description.trim()) continue;
|
|
457
|
+
warnedChildren.add(child.pageDirectory);
|
|
458
|
+
addSectionIssue(context.contentFile, section, {
|
|
459
|
+
severity: 'warning',
|
|
460
|
+
message: `page-list on line ${block.line} includes ${childContext.contentFile.contentLabel} without a non-empty page.description.`,
|
|
461
|
+
fix: 'Add page.description to that child file\'s frontmatter. Explain what the reader will find or when to choose this page, rather than repeating its title.',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
435
464
|
}
|
|
436
465
|
}
|
|
437
466
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
sitePublicLabel,
|
|
10
10
|
} from './lib/site-paths.mjs';
|
|
11
11
|
import { getSiteStructure } from './lib/site-structure.mjs';
|
|
12
|
+
import { getSiteLinkGraph } from './lib/site-link-graph.mjs';
|
|
13
|
+
import { assertCategoryDestinationModel } from './lib/category-destinations.mjs';
|
|
12
14
|
|
|
13
15
|
const keepAstroPublicEntries = new Set([
|
|
14
16
|
'images',
|
|
@@ -53,9 +55,12 @@ for (const generatedFile of generatedPublicFiles) {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
const siteStructure = await getSiteStructure();
|
|
58
|
+
const { categoryModel } = await getSiteLinkGraph({ siteStructure });
|
|
59
|
+
const { destinations: categoryDestinations } = assertCategoryDestinationModel(categoryModel);
|
|
56
60
|
const sitemapXml = createSitemapXml({
|
|
57
61
|
siteStructure,
|
|
58
62
|
siteUrl: projectConfig.site.url,
|
|
63
|
+
categoryDestinations,
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
await mkdir(astroPublicDir, { recursive: true });
|
|
@@ -79,4 +84,5 @@ for (const entry of sourceEntries) {
|
|
|
79
84
|
await writeFile(path.join(astroPublicDir, sitemapFilename), sitemapXml);
|
|
80
85
|
|
|
81
86
|
console.log(`Synced ${sitePublicLabel}/ to ${astroPublicLabel}/.`);
|
|
82
|
-
|
|
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'}.`);
|
|
@@ -32,6 +32,7 @@ const cardImageSizes = [
|
|
|
32
32
|
const displayLink = (link: string) => withBasePath(projectConfig.site.basePath, link);
|
|
33
33
|
const cardListClasses = [
|
|
34
34
|
'card-list',
|
|
35
|
+
'content-block-note-lane-boundary',
|
|
35
36
|
`card-list-layout-${layout}`,
|
|
36
37
|
`card-list-flow-${flow}`,
|
|
37
38
|
`card-list-size-${size}`,
|
|
@@ -14,7 +14,7 @@ const labels = {
|
|
|
14
14
|
<Copy aria-hidden="true" data-code-copy-icon="copy" size={18} stroke-width={2} />
|
|
15
15
|
<Check aria-hidden="true" data-code-copy-icon="copied" hidden size={18} stroke-width={2} />
|
|
16
16
|
<X aria-hidden="true" data-code-copy-icon="failed" hidden size={18} stroke-width={2} />
|
|
17
|
-
<span class="
|
|
17
|
+
<span class="code-block-copy-status" aria-live="polite" data-code-copy-status></span>
|
|
18
18
|
</button>
|
|
19
19
|
</template>
|
|
20
20
|
|
|
@@ -24,6 +24,39 @@ const labels = {
|
|
|
24
24
|
if (!(template instanceof HTMLTemplateElement)) return;
|
|
25
25
|
|
|
26
26
|
const resetTimers = new WeakMap();
|
|
27
|
+
const layoutUpdaters = [];
|
|
28
|
+
const prepareLayout = (wrapper, pre) => {
|
|
29
|
+
const topLevel = wrapper.parentElement?.classList.contains('section-markdown');
|
|
30
|
+
let scheduled = false;
|
|
31
|
+
const update = () => {
|
|
32
|
+
scheduled = false;
|
|
33
|
+
if (topLevel) {
|
|
34
|
+
const layout = ['prose', 'end', 'canvas'].find((candidate) => {
|
|
35
|
+
wrapper.dataset.codeLayout = candidate;
|
|
36
|
+
return pre.scrollWidth <= pre.clientWidth + 1;
|
|
37
|
+
}) ?? 'canvas';
|
|
38
|
+
wrapper.dataset.codeLayout = layout;
|
|
39
|
+
}
|
|
40
|
+
const overflow = pre.scrollWidth > pre.clientWidth + 1;
|
|
41
|
+
wrapper.dataset.codeOverflow = String(overflow);
|
|
42
|
+
if (overflow) pre.setAttribute('tabindex', '0');
|
|
43
|
+
else pre.removeAttribute('tabindex');
|
|
44
|
+
};
|
|
45
|
+
const schedule = () => {
|
|
46
|
+
if (scheduled) return;
|
|
47
|
+
scheduled = true;
|
|
48
|
+
requestAnimationFrame(update);
|
|
49
|
+
};
|
|
50
|
+
layoutUpdaters.push(schedule);
|
|
51
|
+
if ('ResizeObserver' in window) {
|
|
52
|
+
const observer = new ResizeObserver(schedule);
|
|
53
|
+
observer.observe(wrapper.parentElement);
|
|
54
|
+
const body = wrapper.closest('.section-body');
|
|
55
|
+
if (body) observer.observe(body);
|
|
56
|
+
}
|
|
57
|
+
window.addEventListener('resize', schedule, { passive: true });
|
|
58
|
+
update();
|
|
59
|
+
};
|
|
27
60
|
const copyText = async (text) => {
|
|
28
61
|
if (navigator.clipboard?.writeText) {
|
|
29
62
|
try {
|
|
@@ -45,7 +78,7 @@ const labels = {
|
|
|
45
78
|
|
|
46
79
|
const showState = (button, state) => {
|
|
47
80
|
button.querySelectorAll('[data-code-copy-icon]').forEach((icon) => {
|
|
48
|
-
icon.hidden
|
|
81
|
+
icon.toggleAttribute('hidden', icon.getAttribute('data-code-copy-icon') !== state);
|
|
49
82
|
});
|
|
50
83
|
const status = button.querySelector('[data-code-copy-status]');
|
|
51
84
|
if (status) status.textContent = state === 'copied' ? labels.copied : labels.failed;
|
|
@@ -54,11 +87,11 @@ const labels = {
|
|
|
54
87
|
clearTimeout(resetTimers.get(button));
|
|
55
88
|
resetTimers.set(button, setTimeout(() => {
|
|
56
89
|
button.querySelectorAll('[data-code-copy-icon]').forEach((icon) => {
|
|
57
|
-
icon.hidden
|
|
90
|
+
icon.toggleAttribute('hidden', icon.getAttribute('data-code-copy-icon') !== 'copy');
|
|
58
91
|
});
|
|
59
|
-
|
|
92
|
+
// Keep the live announcement available after the visual feedback ends.
|
|
60
93
|
delete button.dataset.copyState;
|
|
61
|
-
}, 2000));
|
|
94
|
+
}, state === 'copied' ? 1000 : 2000));
|
|
62
95
|
};
|
|
63
96
|
|
|
64
97
|
document.querySelectorAll('#main-content .section-markdown pre > code').forEach((code) => {
|
|
@@ -77,10 +110,14 @@ const labels = {
|
|
|
77
110
|
|
|
78
111
|
const button = template.content.firstElementChild?.cloneNode(true);
|
|
79
112
|
if (!(button instanceof HTMLButtonElement)) return;
|
|
80
|
-
|
|
113
|
+
const title = codeExample?.querySelector(':scope > .norna-code-title');
|
|
114
|
+
(title ?? wrapper).append(button);
|
|
115
|
+
prepareLayout(wrapper, pre);
|
|
81
116
|
button.addEventListener('click', async () => {
|
|
82
117
|
if (button.dataset.copyBusy === 'true') return;
|
|
83
118
|
button.dataset.copyBusy = 'true';
|
|
119
|
+
const status = button.querySelector('[data-code-copy-status]');
|
|
120
|
+
if (status) status.textContent = '';
|
|
84
121
|
try {
|
|
85
122
|
await copyText(code.textContent ?? '');
|
|
86
123
|
showState(button, 'copied');
|
|
@@ -91,6 +128,12 @@ const labels = {
|
|
|
91
128
|
}
|
|
92
129
|
});
|
|
93
130
|
});
|
|
131
|
+
const preferences = new MutationObserver(() => layoutUpdaters.forEach((update) => update()));
|
|
132
|
+
preferences.observe(document.documentElement, {
|
|
133
|
+
attributes: true,
|
|
134
|
+
attributeFilter: ['data-reading-width', 'data-focus-reading', 'dir'],
|
|
135
|
+
});
|
|
136
|
+
document.fonts?.ready.then(() => layoutUpdaters.forEach((update) => update()));
|
|
94
137
|
|
|
95
138
|
template.remove();
|
|
96
139
|
})();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CardList from './CardList.astro';
|
|
3
|
+
import ImageCarousel from './ImageCarousel.astro';
|
|
4
|
+
import ImageStack from './ImageStack.astro';
|
|
5
|
+
import PageList from './PageList.astro';
|
|
6
|
+
import type { SectionContentBlock } from '../lib/sectionContent';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
blocks: SectionContentBlock[];
|
|
10
|
+
title: string;
|
|
11
|
+
images: {
|
|
12
|
+
maxAvailableWidthPercent: { desktop: number; mobile: number };
|
|
13
|
+
maxAvailableHeightPercent?: { desktop: number; mobile: number };
|
|
14
|
+
};
|
|
15
|
+
cardWidth: 'text' | 'narrow' | 'normal' | 'wide';
|
|
16
|
+
prioritizeFirstImage?: boolean;
|
|
17
|
+
}
|
|
18
|
+
const { blocks, title, images, cardWidth, prioritizeFirstImage = false } = Astro.props;
|
|
19
|
+
const firstMediaIndex = blocks.findIndex((block) => block.type === 'image-stack' || block.type === 'image-carousel');
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{blocks.map((block, index) => {
|
|
23
|
+
if (block.type === 'html') return <div class="section-markdown" set:html={block.html} />;
|
|
24
|
+
if (block.type === 'tabs') return (
|
|
25
|
+
<div class="content-tabs content-block-note-lane-boundary" data-content-tabs>
|
|
26
|
+
<div class="content-tabs-controls" data-tabs-controls hidden></div>
|
|
27
|
+
{block.panels.map((panel, panelIndex) => (
|
|
28
|
+
<div class="content-tab-panel" data-tab-panel role="group" aria-labelledby={`norna-tabs-label-${block.index}-${panelIndex}`}>
|
|
29
|
+
<div class="content-tab-label" data-tab-label id={`norna-tabs-label-${block.index}-${panelIndex}`}>{panel.label}</div>
|
|
30
|
+
<Astro.self blocks={panel.contentBlocks} title={title} images={images} cardWidth={cardWidth} />
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
if (block.type === 'page-list') return <PageList pages={block.pages} />;
|
|
36
|
+
if (block.type === 'card-list') return <CardList cards={block.cards} layout={block.layout} flow={block.flow} size={block.size} width={block.width ?? cardWidth} />;
|
|
37
|
+
if (block.type === 'image-carousel') return <ImageCarousel title={title} images={block.images} maxAvailableWidthPercent={images.maxAvailableWidthPercent} maxAvailableHeightPercent={images.maxAvailableHeightPercent} priorityFirstImage={prioritizeFirstImage && index === firstMediaIndex} />;
|
|
38
|
+
return <ImageStack title={title} images={block.images} maxAvailableWidthPercent={images.maxAvailableWidthPercent} priorityFirstImage={prioritizeFirstImage && index === firstMediaIndex} />;
|
|
39
|
+
})}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
document.querySelectorAll<HTMLElement>('[data-content-tabs]').forEach((group, groupIndex) => {
|
|
3
|
+
const controls = group.querySelector<HTMLElement>('[data-tabs-controls]')!;
|
|
4
|
+
const panels = Array.from(group.querySelectorAll<HTMLElement>(':scope > [data-tab-panel]'));
|
|
5
|
+
const labels = panels.map((panel) => panel.querySelector<HTMLElement>('[data-tab-label]')!);
|
|
6
|
+
const buttons = panels.map((panel, index) => {
|
|
7
|
+
const button = document.createElement('button');
|
|
8
|
+
const id = `norna-tabs-${groupIndex}-${index}`;
|
|
9
|
+
button.type = 'button';
|
|
10
|
+
button.id = `${id}-tab`;
|
|
11
|
+
button.textContent = labels[index].textContent;
|
|
12
|
+
button.setAttribute('role', 'tab');
|
|
13
|
+
button.setAttribute('aria-controls', id);
|
|
14
|
+
panel.id = id;
|
|
15
|
+
panel.setAttribute('role', 'tabpanel');
|
|
16
|
+
panel.setAttribute('aria-labelledby', button.id);
|
|
17
|
+
panel.tabIndex = 0;
|
|
18
|
+
controls.append(button);
|
|
19
|
+
return button;
|
|
20
|
+
});
|
|
21
|
+
const heading = group.closest('section')?.querySelector<HTMLElement>('h1, h2');
|
|
22
|
+
controls.setAttribute('role', 'tablist');
|
|
23
|
+
if (heading?.id) controls.setAttribute('aria-labelledby', heading.id);
|
|
24
|
+
const select = (selected: number, focus = false) => {
|
|
25
|
+
buttons.forEach((button, index) => {
|
|
26
|
+
button.setAttribute('aria-selected', String(index === selected));
|
|
27
|
+
button.tabIndex = index === selected ? 0 : -1;
|
|
28
|
+
panels[index].hidden = index !== selected;
|
|
29
|
+
});
|
|
30
|
+
if (focus) buttons[selected].focus({ preventScroll: true });
|
|
31
|
+
group.dispatchEvent(new CustomEvent('norna:tab-change', { bubbles: true }));
|
|
32
|
+
window.dispatchEvent(new Event('resize'));
|
|
33
|
+
};
|
|
34
|
+
buttons.forEach((button, index) => {
|
|
35
|
+
button.addEventListener('click', () => select(index));
|
|
36
|
+
button.addEventListener('keydown', (event) => {
|
|
37
|
+
let selected = index;
|
|
38
|
+
if (event.key === 'ArrowRight') selected = (index + 1) % buttons.length;
|
|
39
|
+
else if (event.key === 'ArrowLeft') selected = (index - 1 + buttons.length) % buttons.length;
|
|
40
|
+
else if (event.key === 'Home') selected = 0;
|
|
41
|
+
else if (event.key === 'End') selected = buttons.length - 1;
|
|
42
|
+
else return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
select(selected, true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
controls.hidden = false;
|
|
48
|
+
group.dataset.tabsReady = 'true';
|
|
49
|
+
select(0);
|
|
50
|
+
const revealTarget = () => {
|
|
51
|
+
let id: string;
|
|
52
|
+
try { id = decodeURIComponent(location.hash.slice(1)); } catch { return; }
|
|
53
|
+
const target = document.getElementById(id);
|
|
54
|
+
const index = panels.findIndex((panel) => target && panel.contains(target));
|
|
55
|
+
if (index >= 0) { select(index); target?.scrollIntoView(); }
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener('hashchange', revealTarget);
|
|
58
|
+
revealTarget();
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
@@ -49,25 +49,23 @@ const widthOptions = [
|
|
|
49
49
|
<form class="display-settings-panel">
|
|
50
50
|
<p class="display-settings-title">{labels.displaySettings}</p>
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</fieldset>
|
|
70
|
-
)}
|
|
52
|
+
<fieldset>
|
|
53
|
+
<legend>{labels.appearance}</legend>
|
|
54
|
+
<div class="display-settings-segmented">
|
|
55
|
+
{appearanceOptions.map((option) => (
|
|
56
|
+
<label>
|
|
57
|
+
<input
|
|
58
|
+
type="radio"
|
|
59
|
+
name="appearance"
|
|
60
|
+
value={option.value}
|
|
61
|
+
checked={defaults.appearance === option.value}
|
|
62
|
+
data-reader-appearance
|
|
63
|
+
/>
|
|
64
|
+
<span>{option.label}</span>
|
|
65
|
+
</label>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</fieldset>
|
|
71
69
|
|
|
72
70
|
{controls.readingWidth && (
|
|
73
71
|
<fieldset>
|
|
@@ -45,7 +45,10 @@ const carouselStyle = carouselAspectRatio
|
|
|
45
45
|
: undefined;
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
-
<div
|
|
48
|
+
<div
|
|
49
|
+
class="managed-images content-block-note-lane-boundary"
|
|
50
|
+
aria-label={`${projectConfig.locale.labels.images}: ${title}`}
|
|
51
|
+
>
|
|
49
52
|
<section
|
|
50
53
|
class="managed-image-item image-carousel"
|
|
51
54
|
data-carousel
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { Maximize2 } from '@lucide/astro';
|
|
4
|
+
import { getImageAttributes, getImageInspectionAttributes } from '../lib/generatedImages';
|
|
4
5
|
import {
|
|
5
6
|
getCaptionId,
|
|
6
7
|
getManagedImageSizes,
|
|
@@ -23,21 +24,45 @@ const { title, images, maxAvailableWidthPercent, priorityFirstImage = false } =
|
|
|
23
24
|
const managedImageSizes = getManagedImageSizes(maxAvailableWidthPercent);
|
|
24
25
|
---
|
|
25
26
|
|
|
26
|
-
<div
|
|
27
|
+
<div
|
|
28
|
+
class="image-stack content-block-note-lane-boundary"
|
|
29
|
+
aria-label={`${projectConfig.locale.labels.images}: ${title}`}
|
|
30
|
+
>
|
|
27
31
|
{images.map((image, itemIndex) => {
|
|
28
32
|
const isPriorityItem = priorityFirstImage && itemIndex === 0;
|
|
29
33
|
const captionId = image.caption ? getCaptionId(image, itemIndex) : undefined;
|
|
34
|
+
const inspection = getImageInspectionAttributes(image.src ?? image.image);
|
|
35
|
+
const inspectionDescription = image.alt || image.caption || `${title} ${itemIndex + 1}`;
|
|
36
|
+
const inspectionLabel = projectConfig.locale.labels.inspectImage.replace('{description}', inspectionDescription);
|
|
37
|
+
const inspectionStyle = inspection.width && inspection.height
|
|
38
|
+
? `--managed-image-intrinsic-width: ${inspection.width}px; --managed-image-aspect-ratio: ${inspection.width / inspection.height}`
|
|
39
|
+
: undefined;
|
|
30
40
|
|
|
31
41
|
return (
|
|
32
|
-
<figure class="managed-image-item">
|
|
42
|
+
<figure class="managed-image-item" data-image-stack-figure>
|
|
33
43
|
<div class="managed-image-frame">
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
<a
|
|
45
|
+
class="managed-image-inspection-link"
|
|
46
|
+
href={inspection.href}
|
|
47
|
+
aria-label={inspectionLabel}
|
|
38
48
|
aria-describedby={captionId}
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
data-image-inspection-trigger
|
|
50
|
+
data-image-intrinsic-width={inspection.width}
|
|
51
|
+
data-image-intrinsic-height={inspection.height}
|
|
52
|
+
data-image-inspection-scalable={inspection.scalable ? 'true' : undefined}
|
|
53
|
+
style={inspectionStyle}
|
|
54
|
+
>
|
|
55
|
+
<img
|
|
56
|
+
{...getImageAttributes(image.src ?? image.image, managedImageSizes)}
|
|
57
|
+
class:list={['managed-image', getImageClass(image)]}
|
|
58
|
+
alt={image.alt ?? ''}
|
|
59
|
+
aria-describedby={captionId}
|
|
60
|
+
{...getImageLoadingProps(isPriorityItem)}
|
|
61
|
+
/>
|
|
62
|
+
<span class="managed-image-inspection-affordance" aria-hidden="true">
|
|
63
|
+
<Maximize2 size={18} stroke-width={2} />
|
|
64
|
+
</span>
|
|
65
|
+
</a>
|
|
41
66
|
</div>
|
|
42
67
|
{image.caption && (
|
|
43
68
|
<figcaption class="image-meta" id={captionId}>
|