@abreen/tada 1.16.7 → 1.16.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.16.7",
3
+ "version": "1.16.9",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -4,6 +4,33 @@ export type ContrastPreference = 'standard' | 'high';
4
4
  const FONT_STORAGE_KEY = 'fontPreference';
5
5
  const CONTRAST_STORAGE_KEY = 'contrastPreference';
6
6
 
7
+ interface FontPreferenceRequest {
8
+ generation: number;
9
+ preference: FontPreference;
10
+ promise: Promise<void>;
11
+ }
12
+
13
+ interface FontPreferenceLoader {
14
+ supported: boolean;
15
+ pending: FontPreferenceRequest | null;
16
+ failedPreference: FontPreference | null;
17
+ request(preference: FontPreference): FontPreferenceRequest | null;
18
+ isCurrent(request: FontPreferenceRequest): boolean;
19
+ complete(request: FontPreferenceRequest): void;
20
+ fail(request: FontPreferenceRequest): void;
21
+ cancel(): void;
22
+ }
23
+
24
+ function getFontPreferenceLoader(
25
+ window: Window,
26
+ ): FontPreferenceLoader | undefined {
27
+ return (
28
+ window as Window & {
29
+ __tadaFontPreferenceLoader?: FontPreferenceLoader;
30
+ }
31
+ ).__tadaFontPreferenceLoader;
32
+ }
33
+
7
34
  function getStorage(window: Window): Storage | null {
8
35
  try {
9
36
  return window.localStorage;
@@ -91,6 +118,12 @@ function getDefaultFontPreference(document: Document): FontPreference {
91
118
  : 'sans';
92
119
  }
93
120
 
121
+ function getAppliedFontPreference(document: Document): FontPreference {
122
+ return document.documentElement.dataset.fontPreference === 'serif'
123
+ ? 'serif'
124
+ : 'sans';
125
+ }
126
+
94
127
  function getDefaultContrastPreference(window: Window): ContrastPreference {
95
128
  const configured =
96
129
  window.document.documentElement.dataset.defaultContrastPreference === 'high'
@@ -179,27 +212,78 @@ export default function mountAppearancePicker(window: Window): () => void {
179
212
  container.querySelectorAll<HTMLButtonElement>('button'),
180
213
  );
181
214
  const storage = getStorage(window);
215
+ const fontLoader = getFontPreferenceLoader(window);
182
216
  const defaultFontPreference = getDefaultFontPreference(document);
183
217
  const defaultContrastPreference = getDefaultContrastPreference(window);
184
- let fontPreference = getFontPreference(storage, defaultFontPreference);
218
+ const requestedFontPreference = getFontPreference(
219
+ storage,
220
+ defaultFontPreference,
221
+ );
222
+ let fontPreference = getAppliedFontPreference(document);
185
223
  let contrastPreference = getContrastPreference(
186
224
  storage,
187
225
  defaultContrastPreference,
188
226
  );
189
227
 
190
- applyFontPreference(document, fontPreference);
228
+ const requestedFontIsWaiting =
229
+ (fontLoader?.pending?.preference === requestedFontPreference ||
230
+ fontLoader?.failedPreference === requestedFontPreference) ??
231
+ false;
232
+ if (!requestedFontIsWaiting) {
233
+ fontPreference = requestedFontPreference;
234
+ applyFontPreference(document, fontPreference);
235
+ }
191
236
  applyContrastPreference(document, contrastPreference);
192
237
  syncAppearance(document, fontPreference, contrastPreference);
193
238
 
239
+ const observedRequests = new Set<number>();
240
+ const observeFontRequest = (request: FontPreferenceRequest) => {
241
+ if (observedRequests.has(request.generation)) return;
242
+ observedRequests.add(request.generation);
243
+ request.promise.then(
244
+ () => {
245
+ if (fontLoader?.isCurrent(request)) {
246
+ fontPreference = request.preference;
247
+ saveFontPreference(storage, fontPreference, defaultFontPreference);
248
+ applyFontPreference(document, fontPreference);
249
+ fontLoader.complete(request);
250
+ } else {
251
+ fontPreference = getAppliedFontPreference(document);
252
+ }
253
+ syncAppearance(document, fontPreference, contrastPreference);
254
+ },
255
+ () => {
256
+ fontLoader?.fail(request);
257
+ fontPreference = getAppliedFontPreference(document);
258
+ syncAppearance(document, fontPreference, contrastPreference);
259
+ },
260
+ );
261
+ };
262
+
263
+ if (fontLoader?.pending) {
264
+ observeFontRequest(fontLoader.pending);
265
+ }
266
+
194
267
  const handleClick = (event: Event) => {
195
268
  const button = event.currentTarget as HTMLButtonElement;
196
269
  const fontValue = button.dataset.fontPreferenceValue;
197
270
  const contrastValue = button.dataset.contrastPreferenceValue;
198
271
 
199
272
  if (fontValue === 'sans' || fontValue === 'serif') {
200
- fontPreference = fontValue;
201
- saveFontPreference(storage, fontPreference, defaultFontPreference);
202
- applyFontPreference(document, fontPreference);
273
+ fontPreference = getAppliedFontPreference(document);
274
+ if (fontValue === fontPreference) {
275
+ fontLoader?.cancel();
276
+ saveFontPreference(storage, fontPreference, defaultFontPreference);
277
+ } else {
278
+ const request = fontLoader?.request(fontValue) ?? null;
279
+ if (request) {
280
+ observeFontRequest(request);
281
+ return;
282
+ }
283
+ fontPreference = fontValue;
284
+ saveFontPreference(storage, fontPreference, defaultFontPreference);
285
+ applyFontPreference(document, fontPreference);
286
+ }
203
287
  } else if (contrastValue === 'standard' || contrastValue === 'high') {
204
288
  contrastPreference = contrastValue;
205
289
  saveContrastPreference(
@@ -5,6 +5,9 @@
5
5
  }
6
6
 
7
7
  header {
8
+ --header-search-reservation: 0px;
9
+ --header-to-top-reservation: 0px;
10
+
8
11
  position: fixed;
9
12
  top: 0;
10
13
  z-index: 1000;
@@ -14,19 +17,36 @@ header {
14
17
  background: var(--bg-color-translucent);
15
18
  backdrop-filter: var(--backdrop-filter);
16
19
  view-transition-name: site-header;
20
+
21
+ &:has(.search-controls) {
22
+ --header-search-reservation: 200px;
23
+
24
+ @media (width <= 400px) {
25
+ --header-search-reservation: 0px;
26
+ }
27
+ }
28
+
29
+ &:has(.to-top-container .is-visible) {
30
+ --header-to-top-reservation: 120px;
31
+ }
17
32
  }
18
33
 
19
34
  :root[data-contrast-preference='high'] header {
20
35
  background: var(--bg-color);
21
36
  }
22
37
 
38
+ header > details {
39
+ min-width: 0;
40
+ }
41
+
23
42
  header details summary {
24
43
  position: relative;
25
44
  display: flex;
26
45
  align-items: center;
27
46
  height: var(--header-collapsed-height);
28
- padding: 0 200px 0 0;
29
- container-type: inline-size;
47
+ padding: 0
48
+ calc(var(--header-search-reservation) + var(--header-to-top-reservation)) 0
49
+ 0;
30
50
  cursor: pointer;
31
51
  list-style-type: none;
32
52
 
@@ -47,10 +67,6 @@ header details summary {
47
67
  }
48
68
  }
49
69
 
50
- header:has(.to-top-container .is-visible) details summary {
51
- padding-right: 320px;
52
- }
53
-
54
70
  header details summary::-webkit-details-marker {
55
71
  display: none;
56
72
  }
@@ -105,12 +121,6 @@ header summary .site-title {
105
121
  pointer-events: none;
106
122
  }
107
123
 
108
- @container (max-width: 200px) {
109
- header summary .site-title {
110
- display: none;
111
- }
112
- }
113
-
114
124
  header > .header-controls {
115
125
  position: absolute;
116
126
  top: 0;
@@ -220,7 +220,7 @@
220
220
  }
221
221
 
222
222
  @media print {
223
- .question-a-body {
223
+ .js .question-a-body {
224
224
  cursor: default;
225
225
 
226
226
  @include user-select(text);
package/src/style.scss CHANGED
@@ -93,14 +93,28 @@ a.disabled {
93
93
  }
94
94
 
95
95
  .external-link-tail::after,
96
+ a.external:not(:has(.external-link-tail))::after {
97
+ vertical-align: text-top;
98
+
99
+ @include contained-mask(var(--icon-external-link));
100
+ }
101
+
102
+ .external-link-tail::after {
103
+ display: inline;
104
+
105
+ // With zero font metrics, block-end padding paints down from text-top.
106
+ padding-block-end: var(--icon-external-link-size);
107
+ padding-inline-start: var(--icon-external-link-size);
108
+ font-size: 0;
109
+ line-height: 0;
110
+ content: '\2060' / '';
111
+ }
112
+
96
113
  a.external:not(:has(.external-link-tail))::after {
97
114
  display: inline-block;
98
115
  width: var(--icon-external-link-size);
99
116
  height: var(--icon-external-link-size);
100
- vertical-align: text-top;
101
117
  content: '';
102
-
103
- @include contained-mask(var(--icon-external-link));
104
118
  }
105
119
 
106
120
  .footnotes {
@@ -38,7 +38,70 @@
38
38
  <% } %>
39
39
  <title><%= page.title %><%= site.titlePostfix %></title>
40
40
  <script>
41
+ (() => {
41
42
  document.documentElement.classList.add('js');
43
+ const fontPreferenceFamilies = {
44
+ sans: ['Inter', 'Google Sans Code'],
45
+ serif: [
46
+ '<%= site.fontOverrides?.serif ? 'Tada Custom Serif' : 'Source Serif 4' %>',
47
+ '<%= site.fontOverrides?.serifMono ? 'Tada Custom Serif Mono' : 'Libertinus Mono' %>',
48
+ ],
49
+ };
50
+ const fontPreferenceLoader = {
51
+ generation: 0,
52
+ pending: null,
53
+ failedPreference: null,
54
+ supported:
55
+ document.fonts && typeof document.fonts.load === 'function',
56
+ request(preference) {
57
+ if (!this.supported) return null;
58
+ if (this.pending?.preference === preference) return this.pending;
59
+
60
+ const generation = ++this.generation;
61
+ const [bodyFamily, monoFamily] = fontPreferenceFamilies[preference];
62
+ const promise = Promise.all([
63
+ document.fonts.load('normal 400 1em "' + bodyFamily + '"'),
64
+ document.fonts.load('normal 700 1em "' + bodyFamily + '"'),
65
+ document.fonts.load('normal 400 1em "' + monoFamily + '"'),
66
+ ]).then(matches => {
67
+ if (matches.some(faces => faces.length === 0)) {
68
+ throw new Error('A required font face did not load');
69
+ }
70
+ });
71
+ const request = { generation, preference, promise };
72
+ this.pending = request;
73
+ this.failedPreference = null;
74
+ return request;
75
+ },
76
+ isCurrent(request) {
77
+ return (
78
+ this.pending === request &&
79
+ this.generation === request.generation
80
+ );
81
+ },
82
+ complete(request) {
83
+ if (this.isCurrent(request)) this.pending = null;
84
+ },
85
+ fail(request) {
86
+ if (!this.isCurrent(request)) return;
87
+ this.pending = null;
88
+ this.failedPreference = request.preference;
89
+ },
90
+ cancel() {
91
+ ++this.generation;
92
+ this.pending = null;
93
+ this.failedPreference = null;
94
+ },
95
+ };
96
+ window.__tadaFontPreferenceLoader = fontPreferenceLoader;
97
+
98
+ const applyFontPreference = preference => {
99
+ if (preference === 'serif') {
100
+ document.documentElement.dataset.fontPreference = 'serif';
101
+ } else {
102
+ delete document.documentElement.dataset.fontPreference;
103
+ }
104
+ };
42
105
  try {
43
106
  const preference = window.localStorage.getItem('fontPreference');
44
107
  const configured =
@@ -47,10 +110,27 @@
47
110
  (preference === 'serif' || preference === 'sans') &&
48
111
  preference !== configured
49
112
  ) {
50
- if (preference === 'serif') {
51
- document.documentElement.dataset.fontPreference = 'serif';
113
+ const request = fontPreferenceLoader.request(preference);
114
+ if (!request) {
115
+ applyFontPreference(preference);
52
116
  } else {
53
- delete document.documentElement.dataset.fontPreference;
117
+ request.promise.then(
118
+ () => {
119
+ let storedPreference;
120
+ try {
121
+ storedPreference =
122
+ window.localStorage.getItem('fontPreference');
123
+ } catch {}
124
+ if (
125
+ fontPreferenceLoader.isCurrent(request) &&
126
+ storedPreference === preference
127
+ ) {
128
+ applyFontPreference(preference);
129
+ fontPreferenceLoader.complete(request);
130
+ }
131
+ },
132
+ () => fontPreferenceLoader.fail(request),
133
+ );
54
134
  }
55
135
  }
56
136
  } catch {}
@@ -81,6 +161,7 @@
81
161
  } else {
82
162
  delete document.documentElement.dataset.contrastPreference;
83
163
  }
164
+ })();
84
165
  </script>
85
166
  <% if (!isWatchMode) { %>
86
167
  <script type="speculationrules">