@abreen/tada 1.18.2 → 1.18.4
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 +2 -2
- package/bin/tada.ts +1 -1
- package/build/bundle.ts +3 -3
- package/package.json +1 -1
- package/schema/site.schema.json +1 -1
- package/src/_content.scss +6 -4
- package/src/globals.ts +7 -0
- package/src/header/README.md +8 -2
- package/src/header/_index.scss +77 -31
- package/src/header/index.ts +142 -0
- package/src/navigate/_index.scss +8 -0
- package/src/navigate/runtime.ts +4 -0
- package/templates/_theme.scss +12 -12
- package/templates/_top.html +6 -1
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ Create a new Tada site in a new directory. Prompts for:
|
|
|
109
109
|
- **Site title**: displayed in the header and `<title>` tag
|
|
110
110
|
- **Symbol**: short text (1-5 uppercase characters) shown in the logo and favicon
|
|
111
111
|
- **Theme color**: HSL color, e.g. `hsl(195 70% 40%)`
|
|
112
|
-
- **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `
|
|
112
|
+
- **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `33`)
|
|
113
113
|
- **Background tint amount**: percentage (0-100) of tint to apply (defaults to `100`)
|
|
114
114
|
- **Default time zone**: for `<time>` elements (defaults to your system zone)
|
|
115
115
|
- **Production base URL**: e.g. `https://example.edu`
|
|
@@ -272,7 +272,7 @@ vars:
|
|
|
272
272
|
| `logo` | *Optional*, image path relative to `public/`, replacing the header symbol |
|
|
273
273
|
| `favicon` | *Optional*, `.ico` path relative to `public/`, replacing generated favicons and the web app manifest |
|
|
274
274
|
| `themeColor` | Theme color for the site (e.g., `"tomato"`, `"#c04040"`, `"hsl(195 70% 40%)"`) |
|
|
275
|
-
| `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `
|
|
275
|
+
| `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `33`) |
|
|
276
276
|
| `tintAmount` | *Optional*, percentage (0-100) of tint to apply to the neutral palette (default `100`); link saturation stays fixed |
|
|
277
277
|
| `defaultFont` | *Optional*, initial font pairing: `sans` or `serif` (default `sans`) |
|
|
278
278
|
| `defaultContrast` | *Optional*, initial neutral-palette contrast: `standard` or `high` (default `standard`) |
|
package/bin/tada.ts
CHANGED
package/build/bundle.ts
CHANGED
|
@@ -52,12 +52,12 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
52
52
|
const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
|
|
53
53
|
const template = fs.readFileSync(templatePath, 'utf-8');
|
|
54
54
|
const theme = deriveTheme(siteVariables.themeColor);
|
|
55
|
-
const tintHue = siteVariables.tintHue ??
|
|
55
|
+
const tintHue = siteVariables.tintHue ?? 33;
|
|
56
56
|
const tintAmount = siteVariables.tintAmount ?? 100;
|
|
57
57
|
|
|
58
58
|
const linkHue = formatCssNumber(deriveLinkHue(tintHue));
|
|
59
|
-
const linkColor = `hsl(${linkHue}deg
|
|
60
|
-
const linkColorHover = `hsl(${linkHue}deg
|
|
59
|
+
const linkColor = `hsl(${linkHue}deg 66% 38%)`;
|
|
60
|
+
const linkColorHover = `hsl(${linkHue}deg 56% 49%)`;
|
|
61
61
|
const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
|
|
62
62
|
const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
|
|
63
63
|
const traceLineActiveHue = formatCssNumber(deriveTraceLineActiveHue(tintHue));
|
package/package.json
CHANGED
package/schema/site.schema.json
CHANGED
package/src/_content.scss
CHANGED
|
@@ -56,14 +56,16 @@ a.external:not(:has(.external-link-tail))::after {
|
|
|
56
56
|
padding-top: 32px;
|
|
57
57
|
margin-top: calc(var(--thematic-break-margin) / 2);
|
|
58
58
|
font-size: 0.9em;
|
|
59
|
-
border-top: 3px solid var(--bg2-color);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
.footnotes .title {
|
|
63
62
|
@include user-select(none);
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
font-
|
|
64
|
+
padding: var(--gap) 0;
|
|
65
|
+
font-style: italic;
|
|
66
|
+
color: var(--fg2-color);
|
|
67
|
+
text-align: center;
|
|
68
|
+
border-bottom: 1px solid var(--fg2-color);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
.footnotes p {
|
|
@@ -387,7 +389,7 @@ dfn {
|
|
|
387
389
|
}
|
|
388
390
|
|
|
389
391
|
[aria-current='page'] {
|
|
390
|
-
font-
|
|
392
|
+
font-weight: 700;
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
.material-symbol-icon {
|
package/src/globals.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface Globals {
|
|
|
11
11
|
createResizeObserver: (
|
|
12
12
|
callback: ResizeObserverCallback,
|
|
13
13
|
) => Pick<ResizeObserver, 'disconnect' | 'observe'>;
|
|
14
|
+
createIntersectionObserver: (
|
|
15
|
+
callback: IntersectionObserverCallback,
|
|
16
|
+
options?: IntersectionObserverInit,
|
|
17
|
+
) => Pick<IntersectionObserver, 'disconnect' | 'observe'>;
|
|
14
18
|
fetch: FetchFunction;
|
|
15
19
|
importModule: ImportModuleFunction;
|
|
16
20
|
isDocumentHidden: (document: Document) => boolean;
|
|
@@ -26,6 +30,9 @@ export const globals: Globals = {
|
|
|
26
30
|
createResizeObserver(callback) {
|
|
27
31
|
return new ResizeObserver(callback);
|
|
28
32
|
},
|
|
33
|
+
createIntersectionObserver(callback, options) {
|
|
34
|
+
return new IntersectionObserver(callback, options);
|
|
35
|
+
},
|
|
29
36
|
fetch(input: RequestInfo | URL, init?: RequestInit) {
|
|
30
37
|
return fetch(input, init);
|
|
31
38
|
},
|
package/src/header/README.md
CHANGED
|
@@ -5,6 +5,12 @@ and, where supported, animates its `<details>` content. The component's
|
|
|
5
5
|
JavaScript adds dismissal behavior for outside clicks, focus leaving the menu,
|
|
6
6
|
and the Escape key.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The component also rolls the header title between the site title and the page
|
|
9
|
+
title as the page heading scrolls under the header (see
|
|
10
|
+
[Header Title](../../spec/header-title.md)). The navigator calls
|
|
11
|
+
`swapHeaderTitle()` after each content swap so the header adopts the new page
|
|
12
|
+
title and shows the title for the new scroll position without rolling.
|
|
13
|
+
|
|
9
14
|
If JavaScript is turned off in the web browser, the `<details>` element is still
|
|
10
|
-
functional and its disclosure content opens immediately
|
|
15
|
+
functional and its disclosure content opens immediately, and the header shows
|
|
16
|
+
only the site title.
|
package/src/header/_index.scss
CHANGED
|
@@ -5,32 +5,19 @@
|
|
|
5
5
|
--header-menu-icon-size: 24px;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// The summary and the controls share one grid row, so the site title gets
|
|
9
|
+
// exactly the width the currently rendered controls leave behind.
|
|
8
10
|
header {
|
|
9
|
-
--header-search-reservation: 0px;
|
|
10
|
-
--header-to-top-reservation: 0px;
|
|
11
|
-
|
|
12
11
|
position: fixed;
|
|
13
12
|
top: 0;
|
|
14
13
|
z-index: 1000;
|
|
15
14
|
display: grid;
|
|
16
|
-
grid-template-columns: 1fr;
|
|
15
|
+
grid-template-columns: minmax(0, 1fr) max-content;
|
|
17
16
|
width: 100%;
|
|
17
|
+
container-type: inline-size;
|
|
18
18
|
background: var(--bg-color-translucent);
|
|
19
|
-
border-bottom: 1px solid var(--bg2-color);
|
|
20
19
|
backdrop-filter: var(--backdrop-filter);
|
|
21
20
|
view-transition-name: site-header;
|
|
22
|
-
|
|
23
|
-
&:has(.search-controls) {
|
|
24
|
-
--header-search-reservation: 200px;
|
|
25
|
-
|
|
26
|
-
@media (width <= 400px) {
|
|
27
|
-
--header-search-reservation: 0px;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
&:has(.to-top-container .is-visible) {
|
|
32
|
-
--header-to-top-reservation: 120px;
|
|
33
|
-
}
|
|
34
21
|
}
|
|
35
22
|
|
|
36
23
|
:root[data-contrast-preference='high'] header {
|
|
@@ -57,6 +44,7 @@ body:has(header details[open]) > .container::before {
|
|
|
57
44
|
}
|
|
58
45
|
|
|
59
46
|
header > details {
|
|
47
|
+
grid-area: 1 / 1;
|
|
60
48
|
min-width: 0;
|
|
61
49
|
}
|
|
62
50
|
|
|
@@ -65,9 +53,7 @@ header details summary {
|
|
|
65
53
|
display: flex;
|
|
66
54
|
align-items: center;
|
|
67
55
|
height: var(--header-collapsed-height);
|
|
68
|
-
padding: 0
|
|
69
|
-
calc(var(--header-search-reservation) + var(--header-to-top-reservation)) 0
|
|
70
|
-
0;
|
|
56
|
+
padding: 0;
|
|
71
57
|
cursor: pointer;
|
|
72
58
|
list-style-type: none;
|
|
73
59
|
|
|
@@ -78,7 +64,8 @@ header details summary {
|
|
|
78
64
|
background: var(--bg2-color);
|
|
79
65
|
}
|
|
80
66
|
|
|
81
|
-
&:hover .site-title
|
|
67
|
+
&:hover .site-title,
|
|
68
|
+
&:hover .header-page-title {
|
|
82
69
|
@include link-underline;
|
|
83
70
|
}
|
|
84
71
|
}
|
|
@@ -160,7 +147,9 @@ header details[open] summary .menu-icon-line-bottom {
|
|
|
160
147
|
|
|
161
148
|
:root.js header > details::details-content {
|
|
162
149
|
min-block-size: 0;
|
|
163
|
-
|
|
150
|
+
|
|
151
|
+
// Clip only the height animation; the navigation spans the full header
|
|
152
|
+
overflow: visible clip;
|
|
164
153
|
opacity: 0;
|
|
165
154
|
transform: translateY(-4px);
|
|
166
155
|
transition:
|
|
@@ -176,8 +165,12 @@ header details[open] summary .menu-icon-line-bottom {
|
|
|
176
165
|
}
|
|
177
166
|
}
|
|
178
167
|
|
|
168
|
+
// The details element sits in the first column; its navigation extends under
|
|
169
|
+
// the controls column to fill the header.
|
|
179
170
|
header .content {
|
|
180
171
|
position: relative;
|
|
172
|
+
box-sizing: border-box;
|
|
173
|
+
width: 100cqw;
|
|
181
174
|
}
|
|
182
175
|
|
|
183
176
|
header summary .logo {
|
|
@@ -201,36 +194,89 @@ header summary .logo-image {
|
|
|
201
194
|
object-fit: contain;
|
|
202
195
|
}
|
|
203
196
|
|
|
204
|
-
|
|
197
|
+
// The site title and page title sit on a reel, one header row each, behind a
|
|
198
|
+
// window one row tall. Scrolling past the page heading rolls the reel up to the
|
|
199
|
+
// page title; without JavaScript, only the site title is shown.
|
|
200
|
+
header summary .header-title {
|
|
205
201
|
flex: 1 1 0;
|
|
202
|
+
align-self: stretch;
|
|
206
203
|
min-width: 0;
|
|
207
|
-
margin: 0
|
|
204
|
+
margin: 0 var(--gap) 0 0;
|
|
208
205
|
overflow: hidden;
|
|
209
|
-
text-overflow: ellipsis;
|
|
210
206
|
|
|
211
207
|
// Values above 500 resolve to the supplied bold face when a custom family
|
|
212
208
|
// provides only fixed 400 and 700 weights.
|
|
213
209
|
font-weight: 600;
|
|
214
|
-
white-space: nowrap;
|
|
215
210
|
pointer-events: none;
|
|
216
211
|
}
|
|
217
212
|
|
|
213
|
+
// Feather the window's top and bottom edges so a title fades as it rolls out
|
|
214
|
+
:root.js header summary .header-title {
|
|
215
|
+
-webkit-mask-image: linear-gradient(
|
|
216
|
+
transparent,
|
|
217
|
+
#000 25%,
|
|
218
|
+
#000 75%,
|
|
219
|
+
transparent
|
|
220
|
+
);
|
|
221
|
+
mask-image: linear-gradient(transparent, #000 25%, #000 75%, transparent);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
header summary .header-title-reel {
|
|
225
|
+
display: block;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
header summary .header-title.is-page-title .header-title-reel {
|
|
229
|
+
transform: translateY(calc(-1 * var(--header-collapsed-height)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
233
|
+
header summary .header-title-reel {
|
|
234
|
+
transition: transform 300ms cubic-bezier(0.2, 0, 0, 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
header summary .site-title,
|
|
239
|
+
header summary .header-page-title {
|
|
240
|
+
display: block;
|
|
241
|
+
height: var(--header-collapsed-height);
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
line-height: var(--header-collapsed-height);
|
|
245
|
+
white-space: nowrap;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Code page titles keep their monospace font, without the inline code chip,
|
|
249
|
+
// like the file title heading
|
|
250
|
+
header summary .header-page-title :is(tt, code) {
|
|
251
|
+
padding: 0;
|
|
252
|
+
background: none;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
:root:not(.js) header summary .header-page-title {
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
218
259
|
header > .header-controls {
|
|
219
|
-
position: absolute;
|
|
220
|
-
top: 0;
|
|
221
|
-
right: 0;
|
|
222
|
-
z-index: 1;
|
|
223
260
|
display: flex;
|
|
261
|
+
grid-area: 1 / 2;
|
|
262
|
+
gap: var(--gap);
|
|
224
263
|
align-items: center;
|
|
264
|
+
align-self: start;
|
|
225
265
|
height: var(--header-collapsed-height);
|
|
226
266
|
padding: 0 var(--gap) 0 0;
|
|
227
267
|
}
|
|
228
268
|
|
|
269
|
+
// Continue the summary's hover background behind the controls
|
|
270
|
+
@media (hover: hover) {
|
|
271
|
+
header:has(> details > summary:hover) > .header-controls {
|
|
272
|
+
background: var(--bg2-color);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
229
276
|
header .to-top-container {
|
|
230
277
|
display: none;
|
|
231
278
|
align-items: center;
|
|
232
279
|
height: var(--header-collapsed-height);
|
|
233
|
-
padding: 0 var(--gap) 0 0;
|
|
234
280
|
white-space: nowrap;
|
|
235
281
|
pointer-events: none;
|
|
236
282
|
|
package/src/header/index.ts
CHANGED
|
@@ -1,10 +1,150 @@
|
|
|
1
1
|
import { getElement } from '../util';
|
|
2
|
+
import { globals } from '../globals';
|
|
3
|
+
|
|
4
|
+
export type HeaderTitleState = 'site' | 'page';
|
|
5
|
+
|
|
6
|
+
export interface HeadingGeometry {
|
|
7
|
+
/** Bottom edge of the page heading block (breadcrumbs, h1, and info) */
|
|
8
|
+
blockBottom: number;
|
|
9
|
+
/** Top edge of the page's h1 */
|
|
10
|
+
titleTop: number;
|
|
11
|
+
/** Bottom edge of the collapsed header */
|
|
12
|
+
headerBottom: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HEADING_BLOCK_SELECTOR = '.title-and-info, .file-header';
|
|
16
|
+
const PAGE_TITLE_CLASS = 'is-page-title';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Show the page title once the whole heading block has scrolled under the
|
|
20
|
+
* header, and the site title once the h1 is entirely below the header again.
|
|
21
|
+
* In between, keep the current title so it does not flicker.
|
|
22
|
+
*/
|
|
23
|
+
export function nextHeaderTitleState(
|
|
24
|
+
current: HeaderTitleState,
|
|
25
|
+
geometry: HeadingGeometry | null,
|
|
26
|
+
): HeaderTitleState {
|
|
27
|
+
if (!geometry) {
|
|
28
|
+
return 'site';
|
|
29
|
+
}
|
|
30
|
+
if (geometry.blockBottom <= geometry.headerBottom) {
|
|
31
|
+
return 'page';
|
|
32
|
+
}
|
|
33
|
+
if (geometry.titleTop >= geometry.headerBottom) {
|
|
34
|
+
return 'site';
|
|
35
|
+
}
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getHeadingElements(document: Document) {
|
|
40
|
+
const block = document.querySelector(HEADING_BLOCK_SELECTOR);
|
|
41
|
+
const title = block?.querySelector('h1');
|
|
42
|
+
return block && title ? { block, title } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TitleRoll {
|
|
46
|
+
swap(newDoc: Document): void;
|
|
47
|
+
disconnect(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let titleRoll: TitleRoll | null = null;
|
|
51
|
+
|
|
52
|
+
function getTitleElements(summary: HTMLElement) {
|
|
53
|
+
const container = summary.querySelector<HTMLElement>('.header-title');
|
|
54
|
+
const reel = container?.querySelector<HTMLElement>('.header-title-reel');
|
|
55
|
+
const pageTitle = reel?.querySelector<HTMLElement>('.header-page-title');
|
|
56
|
+
return container && reel && pageTitle ? { container, reel, pageTitle } : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mountTitleRoll(
|
|
60
|
+
window: Window,
|
|
61
|
+
summary: HTMLElement,
|
|
62
|
+
): TitleRoll | null {
|
|
63
|
+
const elements = getTitleElements(summary);
|
|
64
|
+
if (!elements) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const { container, reel, pageTitle } = elements;
|
|
68
|
+
|
|
69
|
+
let state: HeaderTitleState = 'site';
|
|
70
|
+
|
|
71
|
+
function measure(): HeadingGeometry | null {
|
|
72
|
+
const heading = getHeadingElements(window.document);
|
|
73
|
+
// A hidden heading (for example, while presenting slides) has no boxes
|
|
74
|
+
if (!heading || heading.block.getClientRects().length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
blockBottom: heading.block.getBoundingClientRect().bottom,
|
|
79
|
+
titleTop: heading.title.getBoundingClientRect().top,
|
|
80
|
+
headerBottom: summary.getBoundingClientRect().bottom,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Scrolling rolls the reel; other changes, like navigation, switch at once
|
|
85
|
+
function update(roll: boolean) {
|
|
86
|
+
const next = nextHeaderTitleState(state, measure());
|
|
87
|
+
if (next !== state) {
|
|
88
|
+
state = next;
|
|
89
|
+
container.classList.toggle(PAGE_TITLE_CLASS, state === 'page');
|
|
90
|
+
}
|
|
91
|
+
if (!roll && typeof reel.getAnimations === 'function') {
|
|
92
|
+
for (const animation of reel.getAnimations()) {
|
|
93
|
+
animation.finish();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const observer = globals.createIntersectionObserver(() => update(true), {
|
|
99
|
+
rootMargin: `${-summary.getBoundingClientRect().bottom}px 0px 0px 0px`,
|
|
100
|
+
threshold: [0, 1],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
function observeHeading() {
|
|
104
|
+
observer.disconnect();
|
|
105
|
+
const heading = getHeadingElements(window.document);
|
|
106
|
+
if (heading) {
|
|
107
|
+
observer.observe(heading.block);
|
|
108
|
+
observer.observe(heading.title);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
update(false);
|
|
113
|
+
observeHeading();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
swap(newDoc) {
|
|
117
|
+
const newPageTitle = newDoc.querySelector('header .header-page-title');
|
|
118
|
+
if (newPageTitle) {
|
|
119
|
+
pageTitle.replaceChildren(
|
|
120
|
+
...window.document.importNode(newPageTitle, true).childNodes,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
observeHeading();
|
|
124
|
+
update(false);
|
|
125
|
+
},
|
|
126
|
+
disconnect() {
|
|
127
|
+
observer.disconnect();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Called by the navigator once a new page's content and scroll position are in
|
|
134
|
+
* place: adopt the new page title and show the title for the new position
|
|
135
|
+
* without rolling, so the page transition crossfades it.
|
|
136
|
+
*/
|
|
137
|
+
export function swapHeaderTitle(newDoc: Document): void {
|
|
138
|
+
titleRoll?.swap(newDoc);
|
|
139
|
+
}
|
|
2
140
|
|
|
3
141
|
export default (window: Window) => {
|
|
4
142
|
const header: HTMLElement = getElement(window.document, 'header');
|
|
5
143
|
const details = getElement(header, 'details') as HTMLDetailsElement;
|
|
6
144
|
const summary = getElement(details, 'summary') as HTMLElement;
|
|
7
145
|
|
|
146
|
+
titleRoll = mountTitleRoll(window, summary);
|
|
147
|
+
|
|
8
148
|
function close() {
|
|
9
149
|
details.open = false;
|
|
10
150
|
}
|
|
@@ -47,6 +187,8 @@ export default (window: Window) => {
|
|
|
47
187
|
details.addEventListener('focusout', handleDetailsFocusOut);
|
|
48
188
|
|
|
49
189
|
return () => {
|
|
190
|
+
titleRoll?.disconnect();
|
|
191
|
+
titleRoll = null;
|
|
50
192
|
window.removeEventListener('keydown', handleWindowKeyDown);
|
|
51
193
|
window.removeEventListener('click', handleWindowClick);
|
|
52
194
|
details.removeEventListener('click', handleDetailsClick);
|
package/src/navigate/_index.scss
CHANGED
|
@@ -75,6 +75,14 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
75
75
|
inline-size: auto;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// The header stays in place. Parts that change between pages, such as the
|
|
79
|
+
// title shown in it, crossfade in step with the page heading.
|
|
80
|
+
::view-transition-group(site-header),
|
|
81
|
+
::view-transition-old(site-header),
|
|
82
|
+
::view-transition-new(site-header) {
|
|
83
|
+
animation-duration: $duration;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
// The site-wide banner does not change between pages. Keep it out of the root
|
|
79
87
|
// slide and pin its named transition layer in place without a crossfade.
|
|
80
88
|
.site-banner {
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
teardownPerPageComponents,
|
|
6
6
|
} from './lifecycle';
|
|
7
7
|
import { globals } from '../globals';
|
|
8
|
+
import { swapHeaderTitle } from '../header';
|
|
8
9
|
|
|
9
10
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
10
11
|
|
|
@@ -327,6 +328,9 @@ export async function navigateToUrl(
|
|
|
327
328
|
|
|
328
329
|
scrollByIndex.set(historyIndex, window.scrollY);
|
|
329
330
|
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
331
|
+
|
|
332
|
+
// Measured at the destination scroll position; the transition crossfades it
|
|
333
|
+
swapHeaderTitle(newDoc);
|
|
330
334
|
return true;
|
|
331
335
|
};
|
|
332
336
|
|
package/templates/_theme.scss
CHANGED
|
@@ -21,28 +21,28 @@
|
|
|
21
21
|
--note-bg-color: hsl(209deg 95% 75%);
|
|
22
22
|
|
|
23
23
|
// Main foreground color (the color of most text)
|
|
24
|
-
--fg-color: hsl(var(--tint-hue) calc(
|
|
24
|
+
--fg-color: hsl(var(--tint-hue) calc(30% * var(--tint-amount)) 8%);
|
|
25
25
|
--fg-color-translucent: hsl(
|
|
26
26
|
var(--tint-hue) calc(70% * var(--tint-amount)) 8% / 33%
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
// Secondary foreground color (lighter)
|
|
30
|
-
--fg2-color: hsl(var(--tint-hue) calc(
|
|
30
|
+
--fg2-color: hsl(var(--tint-hue) calc(8% * var(--tint-amount)) 45%);
|
|
31
31
|
--link-color: <%= linkColor %>;
|
|
32
32
|
--link-color-hover: <%= linkColorHover %>;
|
|
33
33
|
|
|
34
34
|
// Main background color (the color of the page background)
|
|
35
|
-
--bg-color: hsl(var(--tint-hue) calc(
|
|
35
|
+
--bg-color: hsl(var(--tint-hue) calc(65% * var(--tint-amount)) 97%);
|
|
36
36
|
|
|
37
37
|
// Same saturation as --bg-color, so frosting doesn't add color to the page
|
|
38
38
|
--bg-color-translucent: hsl(
|
|
39
|
-
var(--tint-hue) calc(
|
|
39
|
+
var(--tint-hue) calc(65% * var(--tint-amount)) 95% / 66%
|
|
40
40
|
);
|
|
41
41
|
|
|
42
42
|
// Secondary background color (lighter)
|
|
43
|
-
--bg2-color: hsl(var(--tint-hue) calc(
|
|
43
|
+
--bg2-color: hsl(var(--tint-hue) calc(55% * var(--tint-amount)) 92%);
|
|
44
44
|
--bg2-color-translucent: hsl(
|
|
45
|
-
var(--tint-hue) calc(
|
|
45
|
+
var(--tint-hue) calc(55% * var(--tint-amount)) 92% / 66%
|
|
46
46
|
);
|
|
47
47
|
--bg-trace-line-active: <%= bgTraceLineActive %>;
|
|
48
48
|
--box-shadow: 0 2px 9px
|
|
@@ -110,20 +110,20 @@
|
|
|
110
110
|
--theme-color: <%= themeColorDark %>;
|
|
111
111
|
--theme-color-text: <%= themeColorTextDark %>;
|
|
112
112
|
--text-on-theme: <%= textOnThemeDark %>;
|
|
113
|
-
--fg-color: hsl(var(--tint-hue) calc(
|
|
113
|
+
--fg-color: hsl(var(--tint-hue) calc(40% * var(--tint-amount)) 92%);
|
|
114
114
|
--fg-color-translucent: hsl(
|
|
115
115
|
var(--tint-hue) calc(85% * var(--tint-amount)) 90% / 33%
|
|
116
116
|
);
|
|
117
|
-
--fg2-color: hsl(var(--tint-hue) calc(
|
|
117
|
+
--fg2-color: hsl(var(--tint-hue) calc(10% * var(--tint-amount)) 60%);
|
|
118
118
|
--link-color: <%= linkColorDark %>;
|
|
119
119
|
--link-color-hover: <%= linkColorHoverDark %>;
|
|
120
|
-
--bg-color: hsl(var(--tint-hue) calc(
|
|
120
|
+
--bg-color: hsl(var(--tint-hue) calc(35% * var(--tint-amount)) 5%);
|
|
121
121
|
--bg-color-translucent: hsl(
|
|
122
|
-
var(--tint-hue) calc(
|
|
122
|
+
var(--tint-hue) calc(35% * var(--tint-amount)) 10% / 66%
|
|
123
123
|
);
|
|
124
|
-
--bg2-color: hsl(var(--tint-hue) calc(
|
|
124
|
+
--bg2-color: hsl(var(--tint-hue) calc(40% * var(--tint-amount)) 12%);
|
|
125
125
|
--bg2-color-translucent: hsl(
|
|
126
|
-
var(--tint-hue) calc(
|
|
126
|
+
var(--tint-hue) calc(40% * var(--tint-amount)) 12% / 66%
|
|
127
127
|
);
|
|
128
128
|
--bg-trace-line-active: <%= bgTraceLineActiveDark %>;
|
|
129
129
|
--box-shadow: 0 3px 8px
|
package/templates/_top.html
CHANGED
|
@@ -220,7 +220,12 @@
|
|
|
220
220
|
<path class="menu-icon-line menu-icon-line-bottom" d="M3 18h18" />
|
|
221
221
|
</svg>
|
|
222
222
|
<% if (site.logo) { %><img class="logo-image" src="/<%- encodePublicAssetPath(site.logo) %>" alt="" aria-hidden="true" /><% } else if (site.symbol) { %><span class="logo" aria-hidden="true"><%- site.symbol %></span><% } %>
|
|
223
|
-
<span class="
|
|
223
|
+
<span class="header-title" aria-hidden="true">
|
|
224
|
+
<span class="header-title-reel">
|
|
225
|
+
<span class="site-title"><%- site.title %></span>
|
|
226
|
+
<span class="header-page-title"><%= page.titleHtml %></span>
|
|
227
|
+
</span>
|
|
228
|
+
</span>
|
|
224
229
|
</summary>
|
|
225
230
|
<div class="content">
|
|
226
231
|
<%= render('_nav.html') %>
|