@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="0 50 410 90" xmlns="http://www.w3.org/2000/svg" font-size="14" fill="none" style="margin:1rem auto;display:block;max-width:410px">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.18.1",
3
+ "version": "1.18.2",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
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
- display: grid;
156
- grid-template-columns: 1fr auto;
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
- grid-template-columns: 1fr;
164
+ flex-direction: column;
163
165
  }
164
166
 
165
167
  aside.author {
@@ -43,19 +43,37 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
43
43
  }
44
44
  }
45
45
 
46
- // Page heading and info: crossfade in place instead of sliding with the page
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
- // Breadcrumbs use per-item named rules installed by the navigator. Shared
58
- // items stay opaque and preserve their position; removed/added suffixes fade.
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.
@@ -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
- function cleanupViewTransitionNames(document: Document): void {
131
- const newH1 = document.querySelector(
132
- '.title-and-info h1',
133
- ) as HTMLElement | null;
134
- const newInfo = document.querySelector(
135
- '.title-and-info .info',
136
- ) as HTMLElement | null;
137
-
138
- if (newH1) {
139
- newH1.style.viewTransitionName = '';
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
- if (newInfo) {
142
- newInfo.style.viewTransitionName = '';
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
- const h1 = titleEl.querySelector('h1') as HTMLElement | null;
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
- const h1 = newTitleEl.querySelector('h1') as HTMLElement | null;
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
 
@@ -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(30% * var(--tint-amount)) 95% / 66%
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(15% * var(--tint-amount)) 10% / 66%
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
- }