@abreen/tada 1.18.1 → 1.18.2
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.
|
@@ -18,7 +18,7 @@ The <dfn>standard in</dfn> (or `stdin`) is one of three streams available to a
|
|
|
18
18
|
running program. The other two streams are the standard out and standard error.
|
|
19
19
|
|
|
20
20
|
<figure>
|
|
21
|
-
<svg viewBox="
|
|
21
|
+
<svg viewBox="15 50 380 90" xmlns="http://www.w3.org/2000/svg" font-size="14" fill="none" style="margin:1rem auto;display:block;max-width:380px">
|
|
22
22
|
<defs>
|
|
23
23
|
<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
|
|
24
24
|
<path d="M0,0 L8,4 L0,8 Z" stroke="currentColor" fill="currentColor"></path>
|
package/package.json
CHANGED
package/src/_layout.scss
CHANGED
|
@@ -152,14 +152,16 @@ body.literate main table {
|
|
|
152
152
|
.info {
|
|
153
153
|
@include user-select(none);
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Flex instead of a `1fr auto` grid: WebKit keeps the auto column's width
|
|
156
|
+
// after the font changes, which wraps the author avatar onto its own line.
|
|
157
|
+
display: flex;
|
|
157
158
|
align-items: baseline;
|
|
159
|
+
justify-content: space-between;
|
|
158
160
|
font-size: var(--font-size-smaller);
|
|
159
161
|
color: var(--fg2-color);
|
|
160
162
|
|
|
161
163
|
@media (width < 450px) {
|
|
162
|
-
|
|
164
|
+
flex-direction: column;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
aside.author {
|
package/src/navigate/_index.scss
CHANGED
|
@@ -43,19 +43,37 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Page heading
|
|
46
|
+
// Page heading parts: crossfade in place instead of sliding with the page
|
|
47
47
|
// view-transition-name is set via inline style by the navigator
|
|
48
|
-
// only when .title-and-info is visible in the viewport
|
|
49
|
-
|
|
48
|
+
// only when .title-and-info is visible in the viewport. All parts share one
|
|
49
|
+
// duration and easing so they move in step when the layout changes; the
|
|
50
|
+
// crossfades inherit the group timing.
|
|
51
|
+
::view-transition-group(page-breadcrumbs),
|
|
52
|
+
::view-transition-old(page-breadcrumbs),
|
|
53
|
+
::view-transition-new(page-breadcrumbs),
|
|
54
|
+
::view-transition-group(page-title),
|
|
50
55
|
::view-transition-old(page-title),
|
|
51
56
|
::view-transition-new(page-title),
|
|
57
|
+
::view-transition-group(page-info),
|
|
52
58
|
::view-transition-old(page-info),
|
|
53
59
|
::view-transition-new(page-info) {
|
|
54
60
|
animation-duration: $duration;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
::view-transition-group(page-breadcrumbs),
|
|
64
|
+
::view-transition-group(page-title),
|
|
65
|
+
::view-transition-group(page-info) {
|
|
66
|
+
animation-timing-function: $movement-easing;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Breadcrumbs: the whole trail is one group, so every item moves together when
|
|
70
|
+
// the layout changes. The default plus-lighter crossfade keeps unchanged items
|
|
71
|
+
// opaque while added or removed items fade. Snapshots keep their natural size
|
|
72
|
+
// so a change in trail width does not stretch the text.
|
|
73
|
+
::view-transition-old(page-breadcrumbs),
|
|
74
|
+
::view-transition-new(page-breadcrumbs) {
|
|
75
|
+
inline-size: auto;
|
|
76
|
+
}
|
|
59
77
|
|
|
60
78
|
// The site-wide banner does not change between pages. Keep it out of the root
|
|
61
79
|
// slide and pin its named transition layer in place without a crossfade.
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -5,22 +5,12 @@ 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';
|
|
15
8
|
|
|
16
9
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
17
10
|
|
|
18
11
|
let currentAbortController: AbortController | null = null;
|
|
19
12
|
let historyIndex = 0;
|
|
20
13
|
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 = '';
|
|
24
14
|
|
|
25
15
|
const scrollByIndex = new Map<number, number>();
|
|
26
16
|
const scrollByLocation = new Map<string, number>();
|
|
@@ -127,21 +117,29 @@ function dispatchNavigationEvent(
|
|
|
127
117
|
);
|
|
128
118
|
}
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
120
|
+
// Heading parts that get their own layer instead of sliding with the page. The
|
|
121
|
+
// breadcrumb trail is one group so every item, including the current one,
|
|
122
|
+
// moves together when the page layout changes.
|
|
123
|
+
const TITLE_TRANSITION_NAMES = [
|
|
124
|
+
['nav.breadcrumbs', 'page-breadcrumbs'],
|
|
125
|
+
['h1', 'page-title'],
|
|
126
|
+
['.info', 'page-info'],
|
|
127
|
+
] as const;
|
|
128
|
+
|
|
129
|
+
function setTitleTransitionNames(titleEl: Element, enabled: boolean): void {
|
|
130
|
+
for (const [selector, name] of TITLE_TRANSITION_NAMES) {
|
|
131
|
+
const element = titleEl.querySelector<HTMLElement>(selector);
|
|
132
|
+
if (element) {
|
|
133
|
+
element.style.viewTransitionName = enabled ? name : '';
|
|
134
|
+
}
|
|
140
135
|
}
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function cleanupViewTransitionNames(document: Document): void {
|
|
139
|
+
const titleEl = document.querySelector('.title-and-info');
|
|
140
|
+
if (titleEl) {
|
|
141
|
+
setTitleTransitionNames(titleEl, false);
|
|
143
142
|
}
|
|
144
|
-
cleanupBreadcrumbTransitionNames(document);
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
function getLocationKey(window: Window): string {
|
|
@@ -177,7 +175,6 @@ export function initNavigation(window: Window): void {
|
|
|
177
175
|
scrollByLocation.clear();
|
|
178
176
|
window.history.scrollRestoration = 'manual';
|
|
179
177
|
currentPath = window.location.pathname + window.location.search;
|
|
180
|
-
renderedPageUrl = window.location.href;
|
|
181
178
|
scrollByIndex.set(historyIndex, window.scrollY);
|
|
182
179
|
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
183
180
|
}
|
|
@@ -311,7 +308,6 @@ export async function navigateToUrl(
|
|
|
311
308
|
updateHead(document, newDoc);
|
|
312
309
|
mountAppearancePickerForPage(window);
|
|
313
310
|
currentPath = parsed.pathname + parsed.search;
|
|
314
|
-
renderedPageUrl = parsed.href;
|
|
315
311
|
|
|
316
312
|
if (pushHistory) {
|
|
317
313
|
historyIndex++;
|
|
@@ -343,18 +339,6 @@ export async function navigateToUrl(
|
|
|
343
339
|
!prefersReducedMotion(window) &&
|
|
344
340
|
typeof transitionDoc.startViewTransition === 'function'
|
|
345
341
|
) {
|
|
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
|
-
);
|
|
358
342
|
const titleEl = document.querySelector('.title-and-info');
|
|
359
343
|
const headerHeight =
|
|
360
344
|
document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
@@ -362,20 +346,7 @@ export async function navigateToUrl(
|
|
|
362
346
|
titleEl != null && titleEl.getBoundingClientRect().top >= headerHeight;
|
|
363
347
|
|
|
364
348
|
if (titleVisible) {
|
|
365
|
-
|
|
366
|
-
const info = titleEl.querySelector('.info') as HTMLElement | null;
|
|
367
|
-
if (h1) {
|
|
368
|
-
h1.style.viewTransitionName = 'page-title';
|
|
369
|
-
}
|
|
370
|
-
if (info) {
|
|
371
|
-
info.style.viewTransitionName = 'page-info';
|
|
372
|
-
}
|
|
373
|
-
setBreadcrumbTransitionNames(
|
|
374
|
-
document,
|
|
375
|
-
renderedPageUrl,
|
|
376
|
-
sharedCount,
|
|
377
|
-
'leave',
|
|
378
|
-
);
|
|
349
|
+
setTitleTransitionNames(titleEl, true);
|
|
379
350
|
}
|
|
380
351
|
|
|
381
352
|
document.documentElement.classList.add(
|
|
@@ -395,20 +366,7 @@ export async function navigateToUrl(
|
|
|
395
366
|
newTitleEl.getBoundingClientRect().top >= newHeaderHeight;
|
|
396
367
|
|
|
397
368
|
if (newTitleVisible && newTitleEl) {
|
|
398
|
-
|
|
399
|
-
const info = newTitleEl.querySelector('.info') as HTMLElement | null;
|
|
400
|
-
if (h1) {
|
|
401
|
-
h1.style.viewTransitionName = 'page-title';
|
|
402
|
-
}
|
|
403
|
-
if (info) {
|
|
404
|
-
info.style.viewTransitionName = 'page-info';
|
|
405
|
-
}
|
|
406
|
-
setBreadcrumbTransitionNames(
|
|
407
|
-
document,
|
|
408
|
-
renderedPageUrl,
|
|
409
|
-
sharedCount,
|
|
410
|
-
'enter',
|
|
411
|
-
);
|
|
369
|
+
setTitleTransitionNames(newTitleEl, true);
|
|
412
370
|
}
|
|
413
371
|
});
|
|
414
372
|
|
package/templates/_theme.scss
CHANGED
|
@@ -33,8 +33,10 @@
|
|
|
33
33
|
|
|
34
34
|
// Main background color (the color of the page background)
|
|
35
35
|
--bg-color: hsl(var(--tint-hue) calc(5% * var(--tint-amount)) 97%);
|
|
36
|
+
|
|
37
|
+
// Same saturation as --bg-color, so frosting doesn't add color to the page
|
|
36
38
|
--bg-color-translucent: hsl(
|
|
37
|
-
var(--tint-hue) calc(
|
|
39
|
+
var(--tint-hue) calc(5% * var(--tint-amount)) 95% / 66%
|
|
38
40
|
);
|
|
39
41
|
|
|
40
42
|
// Secondary background color (lighter)
|
|
@@ -117,7 +119,7 @@
|
|
|
117
119
|
--link-color-hover: <%= linkColorHoverDark %>;
|
|
118
120
|
--bg-color: hsl(var(--tint-hue) calc(3% * var(--tint-amount)) 5%);
|
|
119
121
|
--bg-color-translucent: hsl(
|
|
120
|
-
var(--tint-hue) calc(
|
|
122
|
+
var(--tint-hue) calc(3% * var(--tint-amount)) 10% / 66%
|
|
121
123
|
);
|
|
122
124
|
--bg2-color: hsl(var(--tint-hue) calc(12% * var(--tint-amount)) 12%);
|
|
123
125
|
--bg2-color-translucent: hsl(
|
|
@@ -1,129 +0,0 @@
|
|
|
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
|
-
}
|