@abreen/tada 1.18.0 → 1.18.1
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 +1 -1
- package/assets/material-symbols/chevron_right_24px.svg +1 -0
- package/build/material-symbols.ts +7 -4
- package/build/types.d.ts +6 -0
- package/build/utils/render.ts +38 -19
- package/build/validate-config-links.ts +55 -15
- package/init/content/labs/00/VowelCounter.java.md +5 -2
- package/init/content/labs/00/index.md +3 -2
- package/init/content/labs/01/index.md +3 -2
- package/init/content/labs/02/index.md +3 -2
- package/init/content/lectures/01/index.md +3 -2
- package/init/content/lectures/02/index.md +3 -2
- package/package.json +1 -1
- package/src/_content.scss +26 -10
- package/src/_material-symbols.scss +7 -0
- package/src/navigate/_index.scss +9 -3
- package/src/navigate/breadcrumbs.ts +129 -0
- package/src/navigate/runtime.ts +37 -18
- package/templates/_heading.html +15 -2
- package/assets/material-symbols/south_east_20px.svg +0 -1
package/README.md
CHANGED
|
@@ -383,7 +383,7 @@ list of variables parsed using the [`front-matter`][front-matter] library).
|
|
|
383
383
|
| `description` | Meta description for the page |
|
|
384
384
|
| `toc` | Set to `true` to show a table of contents |
|
|
385
385
|
| `slides` | Set to `true` on Markdown pages to treat top-level `---` as slide separators and add presentation controls |
|
|
386
|
-
| `
|
|
386
|
+
| `breadcrumbs` | Ordered ancestor links with `label` and `url`; the page title completes the trail |
|
|
387
387
|
| `published` | Year, month, and day of publishing (e.g, `2025-09-09`) |
|
|
388
388
|
|
|
389
389
|
You may also add arbitrary fields in a page's front matter, and access them
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/></svg>
|
|
@@ -48,10 +48,13 @@ export const MATERIAL_SYMBOLS = Object.freeze({
|
|
|
48
48
|
symbol: 'warning',
|
|
49
49
|
opticalSize: 20,
|
|
50
50
|
}),
|
|
51
|
-
|
|
52
|
-
cssVariable: '--icon-
|
|
53
|
-
symbol: '
|
|
54
|
-
opticalSize:
|
|
51
|
+
breadcrumbSeparator: Object.freeze({
|
|
52
|
+
cssVariable: '--icon-breadcrumb-separator',
|
|
53
|
+
symbol: 'chevron_right',
|
|
54
|
+
opticalSize: 24,
|
|
55
|
+
// The authored selection differs from the shared weight/grade defaults.
|
|
56
|
+
weight: 400,
|
|
57
|
+
grade: 0,
|
|
55
58
|
}),
|
|
56
59
|
search: Object.freeze({
|
|
57
60
|
cssVariable: '--icon-search',
|
package/build/types.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { BundledLanguage } from 'shiki';
|
|
2
2
|
import type { TraceCache } from './build-types';
|
|
3
3
|
|
|
4
|
+
/** An explicitly authored ancestor in a page's breadcrumb trail. */
|
|
5
|
+
export interface BreadcrumbEntry {
|
|
6
|
+
label: string;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export type PlainTextLanguage = 'text' | 'txt' | 'plain';
|
|
5
11
|
|
|
6
12
|
export interface SerifFontTuning {
|
package/build/utils/render.ts
CHANGED
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
getSupportedConfigFileNamesText,
|
|
16
16
|
} from '../config-files';
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
resolveBreadcrumbLinkTarget,
|
|
19
|
+
validateBreadcrumbs,
|
|
20
20
|
} from '../validate-config-links';
|
|
21
21
|
import {
|
|
22
22
|
extractJavaMethodToc,
|
|
@@ -66,8 +66,8 @@ export { stripHtmlComments } from './html-comments';
|
|
|
66
66
|
|
|
67
67
|
const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
69
|
+
// Internal page variables and retired front matter keys. Internal values would
|
|
70
|
+
// be overwritten or, for `template`, select a nonexistent template.
|
|
71
71
|
const RESERVED_FRONT_MATTER_KEYS: readonly string[] = [
|
|
72
72
|
'template',
|
|
73
73
|
'titleHtml',
|
|
@@ -77,6 +77,8 @@ const RESERVED_FRONT_MATTER_KEYS: readonly string[] = [
|
|
|
77
77
|
'codeFilePath',
|
|
78
78
|
'downloadName',
|
|
79
79
|
'filePath',
|
|
80
|
+
'parent',
|
|
81
|
+
'parentLabel',
|
|
80
82
|
];
|
|
81
83
|
|
|
82
84
|
function renderInlineField(
|
|
@@ -456,7 +458,7 @@ interface PreparePageVariablesInput {
|
|
|
456
458
|
* Turns parsed front matter into page variables. Shared by every renderer
|
|
457
459
|
* whose source has front matter, so each one applies the same rules:
|
|
458
460
|
* Lodash processing of string values, inline Markdown for the title and
|
|
459
|
-
* description, required fields, author lookup, and
|
|
461
|
+
* description, required fields, author lookup, and breadcrumb validation.
|
|
460
462
|
*/
|
|
461
463
|
function preparePageVariables({
|
|
462
464
|
rawPageVariables,
|
|
@@ -484,12 +486,29 @@ function preparePageVariables({
|
|
|
484
486
|
subPath,
|
|
485
487
|
isWatchMode,
|
|
486
488
|
});
|
|
489
|
+
const templateString = (value: unknown): unknown =>
|
|
490
|
+
typeof value === 'string' ? compileTemplate(value)(siteOnlyParams) : value;
|
|
487
491
|
const pageVariables: Record<string, unknown> = Object.fromEntries(
|
|
488
|
-
Object.entries(rawPageVariables).map(([
|
|
489
|
-
|
|
490
|
-
|
|
492
|
+
Object.entries(rawPageVariables).map(([key, value]) => [
|
|
493
|
+
key,
|
|
494
|
+
templateString(value),
|
|
491
495
|
]),
|
|
492
496
|
);
|
|
497
|
+
if (Array.isArray(pageVariables.breadcrumbs)) {
|
|
498
|
+
pageVariables.breadcrumbs = pageVariables.breadcrumbs.map(
|
|
499
|
+
(entry: unknown) => {
|
|
500
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
501
|
+
return entry;
|
|
502
|
+
}
|
|
503
|
+
const breadcrumb = entry as Record<string, unknown>;
|
|
504
|
+
return {
|
|
505
|
+
...breadcrumb,
|
|
506
|
+
label: templateString(breadcrumb.label),
|
|
507
|
+
url: templateString(breadcrumb.url),
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
}
|
|
493
512
|
|
|
494
513
|
// Render title and description as inline Markdown
|
|
495
514
|
const frontMatterMd = createMarkdown(siteVariables, {
|
|
@@ -502,21 +521,21 @@ function preparePageVariables({
|
|
|
502
521
|
validateFrontMatter(pageVariables, filePath);
|
|
503
522
|
resolveAuthor(pageVariables, filePath, dependencyCollector);
|
|
504
523
|
|
|
505
|
-
const
|
|
506
|
-
|
|
524
|
+
const breadcrumbs = pageVariables.breadcrumbs;
|
|
525
|
+
validateBreadcrumbs(
|
|
526
|
+
breadcrumbs,
|
|
507
527
|
filePath,
|
|
508
528
|
validInternalTargets,
|
|
509
529
|
sourceUrlPath,
|
|
510
530
|
);
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
dependencyCollector?.internalTargets?.add(resolvedParentTarget);
|
|
531
|
+
for (const entry of breadcrumbs ?? []) {
|
|
532
|
+
const resolvedTarget = resolveBreadcrumbLinkTarget(
|
|
533
|
+
entry.url,
|
|
534
|
+
sourceUrlPath,
|
|
535
|
+
);
|
|
536
|
+
if (resolvedTarget) {
|
|
537
|
+
dependencyCollector?.internalTargets?.add(resolvedTarget);
|
|
538
|
+
}
|
|
520
539
|
}
|
|
521
540
|
|
|
522
541
|
return pageVariables;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import type { BreadcrumbEntry } from './types';
|
|
2
3
|
import { encodeAuthoredUrl } from './template-globals';
|
|
3
4
|
import { normalizeOutputPath } from './utils/paths';
|
|
4
5
|
|
|
@@ -208,47 +209,86 @@ function splitHref(href: string): { pathname: string; suffix: string } {
|
|
|
208
209
|
return { pathname: match ? match[1] : href, suffix: match ? match[2] : '' };
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
export function
|
|
212
|
-
|
|
212
|
+
export function validateBreadcrumbs(
|
|
213
|
+
breadcrumbs: unknown,
|
|
213
214
|
filePath: string,
|
|
214
215
|
validTargets: ReadonlySet<string>,
|
|
215
216
|
sourceUrlPath: string,
|
|
216
|
-
):
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
217
|
+
): asserts breadcrumbs is BreadcrumbEntry[] | undefined {
|
|
218
|
+
if (breadcrumbs === undefined) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!Array.isArray(breadcrumbs)) {
|
|
222
|
+
throw new Error(`${filePath}: breadcrumbs must be a list`);
|
|
219
223
|
}
|
|
224
|
+
breadcrumbs.forEach((entry: unknown, index) => {
|
|
225
|
+
const context = `${filePath}: breadcrumb entry ${index + 1}`;
|
|
226
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
227
|
+
throw new Error(`${context} must be an object with label and url fields`);
|
|
228
|
+
}
|
|
229
|
+
const { label, url } = entry as Record<string, unknown>;
|
|
230
|
+
for (const [field, value] of Object.entries({ label, url })) {
|
|
231
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
232
|
+
throw new Error(`${context}: ${field} must be a nonempty string`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const error = validateBreadcrumbLink(
|
|
236
|
+
url as string,
|
|
237
|
+
filePath,
|
|
238
|
+
validTargets,
|
|
239
|
+
sourceUrlPath,
|
|
240
|
+
index + 1,
|
|
241
|
+
);
|
|
242
|
+
if (error) {
|
|
243
|
+
throw new Error(error);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
220
247
|
|
|
221
|
-
|
|
248
|
+
export function validateBreadcrumbLink(
|
|
249
|
+
url: string,
|
|
250
|
+
filePath: string,
|
|
251
|
+
validTargets: ReadonlySet<string>,
|
|
252
|
+
sourceUrlPath: string,
|
|
253
|
+
entryNumber = 1,
|
|
254
|
+
): string | null {
|
|
255
|
+
const context = `${filePath}: breadcrumb entry ${entryNumber}`;
|
|
256
|
+
const resolvedTarget = resolveBreadcrumbLinkTarget(url, sourceUrlPath);
|
|
222
257
|
if (!resolvedTarget) {
|
|
223
|
-
return
|
|
258
|
+
return `${context}: broken breadcrumb link: "${url}" (must be an internal site link with a pathname)`;
|
|
224
259
|
}
|
|
225
260
|
|
|
226
261
|
const directoryError = validateDirectoryLink(
|
|
227
|
-
|
|
262
|
+
url,
|
|
228
263
|
resolvedTarget,
|
|
229
264
|
validTargets,
|
|
230
|
-
|
|
265
|
+
context,
|
|
231
266
|
);
|
|
232
267
|
if (directoryError) {
|
|
233
268
|
return directoryError;
|
|
234
269
|
}
|
|
235
270
|
|
|
236
271
|
if (!validTargets.has(resolvedTarget)) {
|
|
237
|
-
return `${
|
|
272
|
+
return `${context}: broken breadcrumb link: "${url}"`;
|
|
238
273
|
}
|
|
239
274
|
|
|
240
275
|
return null;
|
|
241
276
|
}
|
|
242
277
|
|
|
243
|
-
export function
|
|
244
|
-
|
|
278
|
+
export function resolveBreadcrumbLinkTarget(
|
|
279
|
+
url: string,
|
|
245
280
|
sourceUrlPath: string,
|
|
246
281
|
): string | null {
|
|
247
|
-
|
|
282
|
+
// Breadcrumbs always point into this site. Backslashes also have special
|
|
283
|
+
// URL semantics in browsers, so they cannot be used as path separators.
|
|
284
|
+
if (
|
|
285
|
+
/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url) ||
|
|
286
|
+
url.startsWith('//') ||
|
|
287
|
+
url.includes('\\')
|
|
288
|
+
) {
|
|
248
289
|
return null;
|
|
249
290
|
}
|
|
250
|
-
|
|
251
|
-
const { pathname } = splitHref(parent);
|
|
291
|
+
const { pathname } = splitHref(url);
|
|
252
292
|
if (!pathname) {
|
|
253
293
|
return null;
|
|
254
294
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
breadcrumbs:
|
|
3
|
+
- label: Labs
|
|
4
|
+
url: /labs/index.html
|
|
5
|
+
- label: Lab 0
|
|
6
|
+
url: /labs/00/index.html
|
|
4
7
|
title: Counting vowels
|
|
5
8
|
author: alex
|
|
6
9
|
description: An example of a literate Java program that takes predefined input.
|
package/package.json
CHANGED
package/src/_content.scss
CHANGED
|
@@ -364,18 +364,34 @@ dfn {
|
|
|
364
364
|
color: var(--theme-color-text);
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
.breadcrumbs {
|
|
368
|
+
ol {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-wrap: wrap;
|
|
371
|
+
gap: 0.25em;
|
|
372
|
+
padding: 0;
|
|
373
|
+
margin: 0;
|
|
374
|
+
list-style: none;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
li {
|
|
378
|
+
display: flex;
|
|
379
|
+
gap: 0.25em;
|
|
380
|
+
align-items: center;
|
|
381
|
+
min-width: 0;
|
|
382
|
+
overflow-wrap: anywhere;
|
|
383
|
+
}
|
|
370
384
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
a {
|
|
386
|
+
@include subdued-link;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
[aria-current='page'] {
|
|
390
|
+
font-style: italic;
|
|
391
|
+
}
|
|
377
392
|
|
|
378
|
-
|
|
393
|
+
.material-symbol-icon {
|
|
394
|
+
flex-shrink: 0;
|
|
379
395
|
}
|
|
380
396
|
}
|
|
381
397
|
|
|
@@ -22,3 +22,10 @@
|
|
|
22
22
|
--material-symbol-icon: var(--icon-contrast-high);
|
|
23
23
|
--material-symbol-icon-size: var(--icon-contrast-high-size);
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
.material-symbol-icon-breadcrumb-separator {
|
|
27
|
+
--material-symbol-icon: var(--icon-breadcrumb-separator);
|
|
28
|
+
--material-symbol-icon-size: var(--icon-breadcrumb-separator-size);
|
|
29
|
+
|
|
30
|
+
color: var(--fg2-color);
|
|
31
|
+
}
|
package/src/navigate/_index.scss
CHANGED
|
@@ -2,6 +2,11 @@ $shift: 8px;
|
|
|
2
2
|
$duration: 150ms;
|
|
3
3
|
$movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
4
4
|
|
|
5
|
+
:root {
|
|
6
|
+
--page-transition-duration: #{$duration};
|
|
7
|
+
--page-transition-movement-easing: #{$movement-easing};
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
@keyframes page-fade-out {
|
|
6
11
|
to {
|
|
7
12
|
opacity: 0;
|
|
@@ -45,12 +50,13 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
45
50
|
::view-transition-old(page-title),
|
|
46
51
|
::view-transition-new(page-title),
|
|
47
52
|
::view-transition-old(page-info),
|
|
48
|
-
::view-transition-new(page-info)
|
|
49
|
-
::view-transition-old(page-breadcrumb),
|
|
50
|
-
::view-transition-new(page-breadcrumb) {
|
|
53
|
+
::view-transition-new(page-info) {
|
|
51
54
|
animation-duration: $duration;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
// Breadcrumbs use per-item named rules installed by the navigator. Shared
|
|
58
|
+
// items stay opaque and preserve their position; removed/added suffixes fade.
|
|
59
|
+
|
|
54
60
|
// The site-wide banner does not change between pages. Keep it out of the root
|
|
55
61
|
// slide and pin its named transition layer in place without a crossfade.
|
|
56
62
|
.site-banner {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
interface BreadcrumbIdentity {
|
|
2
|
+
label: string;
|
|
3
|
+
url: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface BreadcrumbItem extends BreadcrumbIdentity {
|
|
7
|
+
element: HTMLElement;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function breadcrumbPageKey(href: string, pageUrl: string): string {
|
|
11
|
+
const url = new URL(href, pageUrl);
|
|
12
|
+
// Fragments identify positions on the same page; query strings can identify
|
|
13
|
+
// different content. URL serialization also resolves relative paths/spaces.
|
|
14
|
+
return url.origin + url.pathname + url.search;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sharedBreadcrumbCount(
|
|
18
|
+
oldItems: readonly BreadcrumbIdentity[],
|
|
19
|
+
newItems: readonly BreadcrumbIdentity[],
|
|
20
|
+
): number {
|
|
21
|
+
let count = 0;
|
|
22
|
+
while (
|
|
23
|
+
count < Math.min(oldItems.length, newItems.length) &&
|
|
24
|
+
oldItems[count].url === newItems[count].url &&
|
|
25
|
+
oldItems[count].label === newItems[count].label
|
|
26
|
+
) {
|
|
27
|
+
count++;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getBreadcrumbItems(
|
|
33
|
+
document: Document,
|
|
34
|
+
pageUrl: string,
|
|
35
|
+
): BreadcrumbItem[] {
|
|
36
|
+
return Array.from(
|
|
37
|
+
document.querySelectorAll<HTMLElement>('nav.breadcrumbs ol > li'),
|
|
38
|
+
).flatMap(element => {
|
|
39
|
+
const text = element.querySelector('a, [aria-current="page"]');
|
|
40
|
+
if (!text) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const href = text.getAttribute('href') ?? pageUrl;
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
element,
|
|
47
|
+
label: (text.textContent ?? '').trim().replace(/\s+/g, ' '),
|
|
48
|
+
url: breadcrumbPageKey(href, pageUrl),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setBreadcrumbTransitionNames(
|
|
55
|
+
document: Document,
|
|
56
|
+
pageUrl: string,
|
|
57
|
+
sharedCount: number,
|
|
58
|
+
side: 'enter' | 'leave',
|
|
59
|
+
): void {
|
|
60
|
+
const breadcrumb = document.querySelector<HTMLElement>('nav.breadcrumbs');
|
|
61
|
+
if (!breadcrumb) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
getBreadcrumbItems(document, pageUrl).forEach((item, index) => {
|
|
65
|
+
const kind = index < sharedCount ? 'shared' : side;
|
|
66
|
+
item.element.style.viewTransitionName = `page-breadcrumb-${kind}-${index}`;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function prepareBreadcrumbTransitionStyles(
|
|
71
|
+
document: Document,
|
|
72
|
+
oldCount: number,
|
|
73
|
+
newCount: number,
|
|
74
|
+
sharedCount: number,
|
|
75
|
+
): void {
|
|
76
|
+
if (!oldCount && !newCount) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const style = document.createElement('style');
|
|
80
|
+
style.setAttribute('data-breadcrumb-transition-styles', '');
|
|
81
|
+
const rules: string[] = [];
|
|
82
|
+
const duration = 'var(--page-transition-duration)';
|
|
83
|
+
for (let index = 0; index < sharedCount; index++) {
|
|
84
|
+
const name = `page-breadcrumb-shared-${index}`;
|
|
85
|
+
rules.push(`
|
|
86
|
+
::view-transition-group(${name}) {
|
|
87
|
+
animation-duration: ${duration};
|
|
88
|
+
animation-timing-function: var(--page-transition-movement-easing);
|
|
89
|
+
}
|
|
90
|
+
::view-transition-old(${name}), ::view-transition-new(${name}) {
|
|
91
|
+
mix-blend-mode: normal;
|
|
92
|
+
animation: none;
|
|
93
|
+
}
|
|
94
|
+
::view-transition-old(${name}) { display: none; }
|
|
95
|
+
::view-transition-old(${name}):only-child {
|
|
96
|
+
display: block;
|
|
97
|
+
animation: page-fade-out ${duration} ease-in both;
|
|
98
|
+
}
|
|
99
|
+
::view-transition-new(${name}):only-child {
|
|
100
|
+
animation: page-fade-in ${duration} ease-out both;
|
|
101
|
+
}
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
for (let index = sharedCount; index < oldCount; index++) {
|
|
105
|
+
rules.push(`::view-transition-old(page-breadcrumb-leave-${index}) {
|
|
106
|
+
mix-blend-mode: normal;
|
|
107
|
+
animation: page-fade-out ${duration} ease-in both;
|
|
108
|
+
}`);
|
|
109
|
+
}
|
|
110
|
+
for (let index = sharedCount; index < newCount; index++) {
|
|
111
|
+
rules.push(`::view-transition-new(page-breadcrumb-enter-${index}) {
|
|
112
|
+
mix-blend-mode: normal;
|
|
113
|
+
animation: page-fade-in ${duration} ease-out both;
|
|
114
|
+
}`);
|
|
115
|
+
}
|
|
116
|
+
// Exact names keep these rules compatible with the bundled CSS parser, which
|
|
117
|
+
// does not yet accept view-transition-class selectors. Names use indices only.
|
|
118
|
+
style.textContent = rules.join('\n');
|
|
119
|
+
document.head.appendChild(style);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cleanupBreadcrumbTransitionNames(document: Document): void {
|
|
123
|
+
for (const element of document.querySelectorAll<HTMLElement>(
|
|
124
|
+
'nav.breadcrumbs, nav.breadcrumbs li',
|
|
125
|
+
)) {
|
|
126
|
+
element.style.removeProperty('view-transition-name');
|
|
127
|
+
}
|
|
128
|
+
document.querySelector('[data-breadcrumb-transition-styles]')?.remove();
|
|
129
|
+
}
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -5,12 +5,22 @@ import {
|
|
|
5
5
|
teardownPerPageComponents,
|
|
6
6
|
} from './lifecycle';
|
|
7
7
|
import { globals } from '../globals';
|
|
8
|
+
import {
|
|
9
|
+
cleanupBreadcrumbTransitionNames,
|
|
10
|
+
getBreadcrumbItems,
|
|
11
|
+
prepareBreadcrumbTransitionStyles,
|
|
12
|
+
setBreadcrumbTransitionNames,
|
|
13
|
+
sharedBreadcrumbCount,
|
|
14
|
+
} from './breadcrumbs';
|
|
8
15
|
|
|
9
16
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
10
17
|
|
|
11
18
|
let currentAbortController: AbortController | null = null;
|
|
12
19
|
let historyIndex = 0;
|
|
13
20
|
let currentPath = '';
|
|
21
|
+
// Unlike the browser URL (which already changes on popstate), this tracks the
|
|
22
|
+
// page whose content is currently displayed, for relative breadcrumb identity.
|
|
23
|
+
let renderedPageUrl = '';
|
|
14
24
|
|
|
15
25
|
const scrollByIndex = new Map<number, number>();
|
|
16
26
|
const scrollByLocation = new Map<string, number>();
|
|
@@ -124,9 +134,6 @@ function cleanupViewTransitionNames(document: Document): void {
|
|
|
124
134
|
const newInfo = document.querySelector(
|
|
125
135
|
'.title-and-info .info',
|
|
126
136
|
) as HTMLElement | null;
|
|
127
|
-
const newBreadcrumb = document.querySelector(
|
|
128
|
-
'.title-and-info a.breadcrumb',
|
|
129
|
-
) as HTMLElement | null;
|
|
130
137
|
|
|
131
138
|
if (newH1) {
|
|
132
139
|
newH1.style.viewTransitionName = '';
|
|
@@ -134,9 +141,7 @@ function cleanupViewTransitionNames(document: Document): void {
|
|
|
134
141
|
if (newInfo) {
|
|
135
142
|
newInfo.style.viewTransitionName = '';
|
|
136
143
|
}
|
|
137
|
-
|
|
138
|
-
newBreadcrumb.style.viewTransitionName = '';
|
|
139
|
-
}
|
|
144
|
+
cleanupBreadcrumbTransitionNames(document);
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
function getLocationKey(window: Window): string {
|
|
@@ -172,6 +177,7 @@ export function initNavigation(window: Window): void {
|
|
|
172
177
|
scrollByLocation.clear();
|
|
173
178
|
window.history.scrollRestoration = 'manual';
|
|
174
179
|
currentPath = window.location.pathname + window.location.search;
|
|
180
|
+
renderedPageUrl = window.location.href;
|
|
175
181
|
scrollByIndex.set(historyIndex, window.scrollY);
|
|
176
182
|
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
177
183
|
}
|
|
@@ -305,6 +311,7 @@ export async function navigateToUrl(
|
|
|
305
311
|
updateHead(document, newDoc);
|
|
306
312
|
mountAppearancePickerForPage(window);
|
|
307
313
|
currentPath = parsed.pathname + parsed.search;
|
|
314
|
+
renderedPageUrl = parsed.href;
|
|
308
315
|
|
|
309
316
|
if (pushHistory) {
|
|
310
317
|
historyIndex++;
|
|
@@ -336,6 +343,18 @@ export async function navigateToUrl(
|
|
|
336
343
|
!prefersReducedMotion(window) &&
|
|
337
344
|
typeof transitionDoc.startViewTransition === 'function'
|
|
338
345
|
) {
|
|
346
|
+
const oldBreadcrumbItems = getBreadcrumbItems(document, renderedPageUrl);
|
|
347
|
+
const newBreadcrumbItems = getBreadcrumbItems(newDoc, parsed.href);
|
|
348
|
+
const sharedCount = sharedBreadcrumbCount(
|
|
349
|
+
oldBreadcrumbItems,
|
|
350
|
+
newBreadcrumbItems,
|
|
351
|
+
);
|
|
352
|
+
prepareBreadcrumbTransitionStyles(
|
|
353
|
+
document,
|
|
354
|
+
oldBreadcrumbItems.length,
|
|
355
|
+
newBreadcrumbItems.length,
|
|
356
|
+
sharedCount,
|
|
357
|
+
);
|
|
339
358
|
const titleEl = document.querySelector('.title-and-info');
|
|
340
359
|
const headerHeight =
|
|
341
360
|
document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
@@ -345,18 +364,18 @@ export async function navigateToUrl(
|
|
|
345
364
|
if (titleVisible) {
|
|
346
365
|
const h1 = titleEl.querySelector('h1') as HTMLElement | null;
|
|
347
366
|
const info = titleEl.querySelector('.info') as HTMLElement | null;
|
|
348
|
-
const breadcrumb = titleEl.querySelector(
|
|
349
|
-
'a.breadcrumb',
|
|
350
|
-
) as HTMLElement | null;
|
|
351
367
|
if (h1) {
|
|
352
368
|
h1.style.viewTransitionName = 'page-title';
|
|
353
369
|
}
|
|
354
370
|
if (info) {
|
|
355
371
|
info.style.viewTransitionName = 'page-info';
|
|
356
372
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
373
|
+
setBreadcrumbTransitionNames(
|
|
374
|
+
document,
|
|
375
|
+
renderedPageUrl,
|
|
376
|
+
sharedCount,
|
|
377
|
+
'leave',
|
|
378
|
+
);
|
|
360
379
|
}
|
|
361
380
|
|
|
362
381
|
document.documentElement.classList.add(
|
|
@@ -378,18 +397,18 @@ export async function navigateToUrl(
|
|
|
378
397
|
if (newTitleVisible && newTitleEl) {
|
|
379
398
|
const h1 = newTitleEl.querySelector('h1') as HTMLElement | null;
|
|
380
399
|
const info = newTitleEl.querySelector('.info') as HTMLElement | null;
|
|
381
|
-
const breadcrumb = newTitleEl.querySelector(
|
|
382
|
-
'a.breadcrumb',
|
|
383
|
-
) as HTMLElement | null;
|
|
384
400
|
if (h1) {
|
|
385
401
|
h1.style.viewTransitionName = 'page-title';
|
|
386
402
|
}
|
|
387
403
|
if (info) {
|
|
388
404
|
info.style.viewTransitionName = 'page-info';
|
|
389
405
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
406
|
+
setBreadcrumbTransitionNames(
|
|
407
|
+
document,
|
|
408
|
+
renderedPageUrl,
|
|
409
|
+
sharedCount,
|
|
410
|
+
'enter',
|
|
411
|
+
);
|
|
393
412
|
}
|
|
394
413
|
});
|
|
395
414
|
|
package/templates/_heading.html
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
<div class="title-and-info" data-pagefind-body>
|
|
2
|
-
<% if (page.
|
|
3
|
-
<
|
|
2
|
+
<% if (page.breadcrumbs && page.breadcrumbs.length) { %>
|
|
3
|
+
<nav class="breadcrumbs" aria-label="Breadcrumb" data-pagefind-ignore>
|
|
4
|
+
<ol>
|
|
5
|
+
<% page.breadcrumbs.forEach((entry, index) => { %>
|
|
6
|
+
<li>
|
|
7
|
+
<% if (index > 0) { %><span class="material-symbol-icon material-symbol-icon-breadcrumb-separator" aria-hidden="true"></span><% } %>
|
|
8
|
+
<a href="<%- encodeAuthoredUrl(entry.url) %>"><%- entry.label %></a>
|
|
9
|
+
</li>
|
|
10
|
+
<% }); %>
|
|
11
|
+
<li>
|
|
12
|
+
<span class="material-symbol-icon material-symbol-icon-breadcrumb-separator" aria-hidden="true"></span>
|
|
13
|
+
<span aria-current="page"><%- page.title %></span>
|
|
14
|
+
</li>
|
|
15
|
+
</ol>
|
|
16
|
+
</nav>
|
|
4
17
|
<% } %>
|
|
5
18
|
<h1 data-pagefind-weight="10" data-pagefind-meta="title_html[data-title-html]" data-title-html="<%- page.titleHtml %>"><%= page.titleHtml %></h1>
|
|
6
19
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" width="20"><path d="M386-224v-58h250.69L200-718.69 241.31-760 678-323.31V-574h58v350H386Z"/></svg>
|