@abreen/tada 1.16.0 → 1.16.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 CHANGED
@@ -219,6 +219,8 @@ titlePostfix: " - CS 0"
219
219
  symbol: CS 0
220
220
  themeColor: hsl(351 70% 40%)
221
221
  tintAmount: 0
222
+ defaultFont: serif
223
+ defaultContrast: high
222
224
  features:
223
225
  search: true
224
226
  favicon: true
@@ -245,6 +247,8 @@ vars:
245
247
  | `themeColor` | Theme color for the site (e.g., `"tomato"`, `"#c04040"`, `"hsl(195 70% 40%)"`) |
246
248
  | `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `20`) |
247
249
  | `tintAmount` | *Optional*, percentage (0-100) of tint to apply (default `100`) |
250
+ | `defaultFont` | *Optional*, initial font pairing: `sans` or `serif` (default `sans`) |
251
+ | `defaultContrast` | *Optional*, initial neutral-palette contrast: `standard` or `high` (default `standard`) |
248
252
  | `faviconSymbol` | *Optional*, the text to use instead of `symbol` in the favicon |
249
253
  | `features.search` | Enable search UI and Pagefind index generation |
250
254
  | `features.favicon` | Enable automatically generated favicons |
package/bin/validators.ts CHANGED
@@ -78,6 +78,8 @@ export function createSiteConfig({
78
78
  basePath,
79
79
  internalDomains,
80
80
  defaultTimeZone,
81
+ defaultFont: 'sans',
82
+ defaultContrast: 'standard',
81
83
  extensionToShikiLanguage,
82
84
  shikiLanguages,
83
85
  themeColor,
@@ -6,6 +6,23 @@ import { makeLogger } from './log';
6
6
  const log = makeLogger(import.meta.url);
7
7
  const FONTS_DIR = path.join(getPackageDir(), 'fonts');
8
8
 
9
+ export const DEFAULT_FONT_PRELOAD_FILES = {
10
+ sans: [
11
+ 'inter/InterVariable.woff2',
12
+ 'google-sans-code/GoogleSansCodeVariable.woff2',
13
+ ],
14
+ serif: [
15
+ 'source-serif-4/SourceSerif4-VariableFont_opsz,wght.woff2',
16
+ 'libertinus-mono/LibertinusMono-Regular.woff2',
17
+ ],
18
+ } as const;
19
+
20
+ export function getDefaultFontPreloadFiles(
21
+ defaultFont: 'sans' | 'serif' | undefined,
22
+ ): readonly string[] {
23
+ return DEFAULT_FONT_PRELOAD_FILES[defaultFont ?? 'sans'];
24
+ }
25
+
9
26
  export function copyFonts(distDir: string): void {
10
27
  log.info`Copying fonts`;
11
28
 
@@ -12,6 +12,8 @@ const configDir = getProjectDir();
12
12
 
13
13
  const DEFAULT: Partial<SiteVariables> = {
14
14
  basePath: '/',
15
+ defaultFont: 'sans',
16
+ defaultContrast: 'standard',
15
17
  features: { search: true, favicon: true, footer: true },
16
18
  };
17
19
 
package/build/types.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface SiteVariables {
16
16
  faviconFontWeight?: number;
17
17
  internalDomains?: string[];
18
18
  defaultTimeZone: string;
19
+ defaultFont?: 'sans' | 'serif';
20
+ defaultContrast?: 'standard' | 'high';
19
21
  features: FeatureConfig;
20
22
  extensionToShikiLanguage?: Record<
21
23
  string,
@@ -7,6 +7,7 @@ import { makeLogger } from '../log';
7
7
  import { B } from '../colors';
8
8
  import createTemplateGlobals from '../template-globals';
9
9
  import { getExtensionToShikiLanguage } from '../site-variables';
10
+ import { getDefaultFontPreloadFiles } from '../generate-fonts';
10
11
  import { config } from '../templates';
11
12
  import { render } from '../templates';
12
13
  import {
@@ -157,6 +158,7 @@ export function injectAssetTags(
157
158
  html: string,
158
159
  assetFiles: string[],
159
160
  distDir: string,
161
+ siteVariables: SiteVariables,
160
162
  ): string {
161
163
  const jsAssets = assetFiles.filter(f => f.endsWith('.js'));
162
164
  const cssAssets = assetFiles.filter(f => f.endsWith('.css'));
@@ -182,10 +184,7 @@ export function injectAssetTags(
182
184
  )
183
185
  .join('');
184
186
 
185
- const fontPreloadTags = [
186
- 'inter/InterVariable.woff2',
187
- 'google-sans-code/GoogleSansCodeVariable.woff2',
188
- ]
187
+ const fontPreloadTags = getDefaultFontPreloadFiles(siteVariables.defaultFont)
189
188
  .filter(f => fs.existsSync(path.join(distDir, f)))
190
189
  .map(
191
190
  f =>
@@ -210,12 +209,14 @@ export function preparePageTemplateHtml({
210
209
  templateHtml,
211
210
  assetFiles,
212
211
  distDir,
212
+ siteVariables,
213
213
  }: {
214
214
  templateHtml: string;
215
215
  assetFiles: string[];
216
216
  distDir: string;
217
+ siteVariables: SiteVariables;
217
218
  }): string {
218
- let html = injectAssetTags(templateHtml, assetFiles, distDir);
219
+ let html = injectAssetTags(templateHtml, assetFiles, distDir, siteVariables);
219
220
  if (templateHtml.includes('class="katex"')) {
220
221
  html = injectKatexStylesheet(html);
221
222
  }
@@ -289,7 +290,12 @@ export function renderPlainTextPageAsset({
289
290
  ) as string;
290
291
  const finalized = finalizeHtmlPage({
291
292
  filePath,
292
- html: preparePageTemplateHtml({ templateHtml, assetFiles, distDir }),
293
+ html: preparePageTemplateHtml({
294
+ templateHtml,
295
+ assetFiles,
296
+ distDir,
297
+ siteVariables,
298
+ }),
293
299
  siteVariables,
294
300
  sourceUrlPath,
295
301
  validInternalTargets,
@@ -362,7 +368,12 @@ export function renderCodePageAsset({
362
368
  const templateHtml = render('code.html', templateParameters) as string;
363
369
  const finalized = finalizeHtmlPage({
364
370
  filePath,
365
- html: preparePageTemplateHtml({ templateHtml, assetFiles, distDir }),
371
+ html: preparePageTemplateHtml({
372
+ templateHtml,
373
+ assetFiles,
374
+ distDir,
375
+ siteVariables,
376
+ }),
366
377
  siteVariables,
367
378
  sourceUrlPath,
368
379
  validInternalTargets,
@@ -700,7 +711,12 @@ export function renderLiterateJavaPageAsset({
700
711
  const templateHtml = render('literate.html', templateParameters) as string;
701
712
  const finalized = finalizeHtmlPage({
702
713
  filePath,
703
- html: preparePageTemplateHtml({ templateHtml, assetFiles, distDir }),
714
+ html: preparePageTemplateHtml({
715
+ templateHtml,
716
+ assetFiles,
717
+ distDir,
718
+ siteVariables,
719
+ }),
704
720
  siteVariables,
705
721
  sourceUrlPath,
706
722
  validInternalTargets,
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { bundle, getBundleNaming } from '../bundle';
4
- import { copyFonts } from '../generate-fonts';
4
+ import { copyFonts, DEFAULT_FONT_PRELOAD_FILES } from '../generate-fonts';
5
5
  import { copyKatexAssets } from '../generate-katex-assets';
6
6
  import { generateFavicons } from '../generate-favicon';
7
7
  import { generateWebAppManifest } from '../generate-web-app-manifest';
@@ -72,8 +72,8 @@ export function copyExistingBuildAssets(
72
72
  ): void {
73
73
  const relPaths = [
74
74
  ...assetFiles,
75
- 'inter/InterVariable.woff2',
76
- 'google-sans-code/GoogleSansCodeVariable.woff2',
75
+ ...DEFAULT_FONT_PRELOAD_FILES.sans,
76
+ ...DEFAULT_FONT_PRELOAD_FILES.serif,
77
77
  ];
78
78
 
79
79
  for (const relPath of relPaths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.16.0",
3
+ "version": "1.16.1",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -48,6 +48,16 @@
48
48
  "default": 700
49
49
  },
50
50
  "defaultTimeZone": { "type": "string" },
51
+ "defaultFont": {
52
+ "type": "string",
53
+ "enum": ["sans", "serif"],
54
+ "default": "sans"
55
+ },
56
+ "defaultContrast": {
57
+ "type": "string",
58
+ "enum": ["standard", "high"],
59
+ "default": "standard"
60
+ },
51
61
  "vars": { "type": "object" },
52
62
  "extensionToShikiLanguage": {
53
63
  "type": "object",
@@ -15,11 +15,14 @@ function getStorage(window: Window): Storage | null {
15
15
  function getStoredPreference<T extends string>(
16
16
  storage: Storage | null,
17
17
  key: string,
18
- storedValue: T,
18
+ storedValues: readonly T[],
19
19
  defaultValue: T,
20
20
  ): T {
21
21
  try {
22
- return storage?.getItem(key) === storedValue ? storedValue : defaultValue;
22
+ const storedValue = storage?.getItem(key);
23
+ return storedValues.includes(storedValue as T)
24
+ ? (storedValue as T)
25
+ : defaultValue;
23
26
  } catch {
24
27
  return defaultValue;
25
28
  }
@@ -29,41 +32,69 @@ function saveStoredPreference<T extends string>(
29
32
  storage: Storage | null,
30
33
  key: string,
31
34
  preference: T,
32
- storedValue: T,
35
+ defaultValue: T,
33
36
  ): void {
34
37
  try {
35
- if (preference === storedValue) {
36
- storage?.setItem(key, preference);
37
- } else {
38
+ if (preference === defaultValue) {
38
39
  storage?.removeItem(key);
40
+ } else {
41
+ storage?.setItem(key, preference);
39
42
  }
40
43
  } catch {
41
44
  // Storage can be unavailable in privacy-restricted contexts.
42
45
  }
43
46
  }
44
47
 
45
- export function getFontPreference(storage: Storage | null): FontPreference {
46
- return getStoredPreference(storage, FONT_STORAGE_KEY, 'serif', 'sans');
48
+ export function getFontPreference(
49
+ storage: Storage | null,
50
+ defaultValue: FontPreference = 'sans',
51
+ ): FontPreference {
52
+ return getStoredPreference(
53
+ storage,
54
+ FONT_STORAGE_KEY,
55
+ ['sans', 'serif'],
56
+ defaultValue,
57
+ );
47
58
  }
48
59
 
49
60
  export function getContrastPreference(
50
61
  storage: Storage | null,
62
+ defaultValue: ContrastPreference = 'standard',
51
63
  ): ContrastPreference {
52
- return getStoredPreference(storage, CONTRAST_STORAGE_KEY, 'high', 'standard');
64
+ return getStoredPreference(
65
+ storage,
66
+ CONTRAST_STORAGE_KEY,
67
+ ['standard', 'high'],
68
+ defaultValue,
69
+ );
53
70
  }
54
71
 
55
72
  export function saveFontPreference(
56
73
  storage: Storage | null,
57
74
  preference: FontPreference,
75
+ defaultValue: FontPreference = 'sans',
58
76
  ): void {
59
- saveStoredPreference(storage, FONT_STORAGE_KEY, preference, 'serif');
77
+ saveStoredPreference(storage, FONT_STORAGE_KEY, preference, defaultValue);
60
78
  }
61
79
 
62
80
  export function saveContrastPreference(
63
81
  storage: Storage | null,
64
82
  preference: ContrastPreference,
83
+ defaultValue: ContrastPreference = 'standard',
65
84
  ): void {
66
- saveStoredPreference(storage, CONTRAST_STORAGE_KEY, preference, 'high');
85
+ saveStoredPreference(storage, CONTRAST_STORAGE_KEY, preference, defaultValue);
86
+ }
87
+
88
+ function getDefaultFontPreference(document: Document): FontPreference {
89
+ return document.documentElement.dataset.defaultFontPreference === 'serif'
90
+ ? 'serif'
91
+ : 'sans';
92
+ }
93
+
94
+ function getDefaultContrastPreference(document: Document): ContrastPreference {
95
+ return document.documentElement.dataset.defaultContrastPreference === 'high'
96
+ ? 'high'
97
+ : 'standard';
67
98
  }
68
99
 
69
100
  export function applyFontPreference(
@@ -71,8 +102,10 @@ export function applyFontPreference(
71
102
  preference: FontPreference,
72
103
  ): void {
73
104
  if (preference === 'serif') {
74
- document.documentElement.dataset.fontPreference = preference;
75
- } else {
105
+ if (document.documentElement.dataset.fontPreference !== preference) {
106
+ document.documentElement.dataset.fontPreference = preference;
107
+ }
108
+ } else if (document.documentElement.dataset.fontPreference !== undefined) {
76
109
  delete document.documentElement.dataset.fontPreference;
77
110
  }
78
111
  }
@@ -82,8 +115,12 @@ export function applyContrastPreference(
82
115
  preference: ContrastPreference,
83
116
  ): void {
84
117
  if (preference === 'high') {
85
- document.documentElement.dataset.contrastPreference = preference;
86
- } else {
118
+ if (document.documentElement.dataset.contrastPreference !== preference) {
119
+ document.documentElement.dataset.contrastPreference = preference;
120
+ }
121
+ } else if (
122
+ document.documentElement.dataset.contrastPreference !== undefined
123
+ ) {
87
124
  delete document.documentElement.dataset.contrastPreference;
88
125
  }
89
126
  }
@@ -129,8 +166,13 @@ export default function mountAppearancePicker(window: Window): () => void {
129
166
  container.querySelectorAll<HTMLButtonElement>('button'),
130
167
  );
131
168
  const storage = getStorage(window);
132
- let fontPreference = getFontPreference(storage);
133
- let contrastPreference = getContrastPreference(storage);
169
+ const defaultFontPreference = getDefaultFontPreference(document);
170
+ const defaultContrastPreference = getDefaultContrastPreference(document);
171
+ let fontPreference = getFontPreference(storage, defaultFontPreference);
172
+ let contrastPreference = getContrastPreference(
173
+ storage,
174
+ defaultContrastPreference,
175
+ );
134
176
 
135
177
  applyFontPreference(document, fontPreference);
136
178
  applyContrastPreference(document, contrastPreference);
@@ -143,11 +185,15 @@ export default function mountAppearancePicker(window: Window): () => void {
143
185
 
144
186
  if (fontValue === 'sans' || fontValue === 'serif') {
145
187
  fontPreference = fontValue;
146
- saveFontPreference(storage, fontPreference);
188
+ saveFontPreference(storage, fontPreference, defaultFontPreference);
147
189
  applyFontPreference(document, fontPreference);
148
190
  } else if (contrastValue === 'standard' || contrastValue === 'high') {
149
191
  contrastPreference = contrastValue;
150
- saveContrastPreference(storage, contrastPreference);
192
+ saveContrastPreference(
193
+ storage,
194
+ contrastPreference,
195
+ defaultContrastPreference,
196
+ );
151
197
  applyContrastPreference(document, contrastPreference);
152
198
  } else {
153
199
  return;
@@ -75,8 +75,10 @@
75
75
  }
76
76
 
77
77
  .font-picker-option-sans {
78
- font-family: var(--sans-font);
79
- font-feature-settings: var(--sans-font-feature-settings);
78
+ font-family:
79
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
80
+ sans-serif;
81
+ font-feature-settings: normal;
80
82
  }
81
83
 
82
84
  .font-picker-option-serif {
@@ -86,6 +88,15 @@
86
88
  font-feature-settings: var(--serif-font-feature-settings);
87
89
  }
88
90
 
91
+ :root:not([data-font-preference='serif']) .font-picker-option-sans {
92
+ font-family: var(--sans-font);
93
+ font-feature-settings: var(--sans-font-feature-settings);
94
+ }
95
+
96
+ :root[data-font-preference='serif'] .font-picker-option-serif {
97
+ font-family: var(--serif-font);
98
+ }
99
+
89
100
  .material-symbol-icon {
90
101
  width: 1.25rem;
91
102
  height: 1.25rem;
@@ -16,6 +16,10 @@ header {
16
16
  view-transition-name: site-header;
17
17
  }
18
18
 
19
+ :root[data-contrast-preference='high'] header {
20
+ background: var(--bg-color);
21
+ }
22
+
19
23
  header details summary {
20
24
  position: relative;
21
25
  display: flex;
@@ -50,10 +50,18 @@ header nav div > p {
50
50
  @include user-select(none);
51
51
  }
52
52
 
53
+ :root[data-contrast-preference='high'] header:has(details[open]) {
54
+ background: var(--bg2-color);
55
+ }
56
+
53
57
  @media (hover: hover) {
54
58
  header:has(details[open]):hover {
55
59
  background: var(--bg-color-translucent);
56
60
  }
61
+
62
+ :root[data-contrast-preference='high'] header:hover {
63
+ background: var(--bg2-color);
64
+ }
57
65
  }
58
66
 
59
67
  header .results-container {
@@ -1,17 +1,17 @@
1
1
  <div class="appearance-pickers" data-pagefind-ignore>
2
2
  <div class="preference-picker font-picker" role="group" aria-label="Font style">
3
- <button type="button" class="preference-picker-option font-picker-option-sans" data-font-preference-value="sans" aria-label="Use sans-serif fonts" aria-pressed="true" disabled>
3
+ <button type="button" class="preference-picker-option font-picker-option-sans" data-font-preference-value="sans" aria-label="Use sans-serif fonts" aria-pressed="<%= site.defaultFont === 'sans' %>" disabled>
4
4
  <span aria-hidden="true">Aa</span>
5
5
  </button>
6
- <button type="button" class="preference-picker-option font-picker-option-serif" data-font-preference-value="serif" aria-label="Use serif fonts" aria-pressed="false" disabled>
6
+ <button type="button" class="preference-picker-option font-picker-option-serif" data-font-preference-value="serif" aria-label="Use serif fonts" aria-pressed="<%= site.defaultFont === 'serif' %>" disabled>
7
7
  <span aria-hidden="true">Aa</span>
8
8
  </button>
9
9
  </div>
10
10
  <div class="preference-picker contrast-picker" role="group" aria-label="Contrast">
11
- <button type="button" class="preference-picker-option contrast-picker-option-standard" data-contrast-preference-value="standard" aria-label="Use standard contrast" aria-pressed="true" disabled>
11
+ <button type="button" class="preference-picker-option contrast-picker-option-standard" data-contrast-preference-value="standard" aria-label="Use standard contrast" aria-pressed="<%= site.defaultContrast === 'standard' %>" disabled>
12
12
  <span class="material-symbol-icon material-symbol-icon-contrast-standard" aria-hidden="true"></span>
13
13
  </button>
14
- <button type="button" class="preference-picker-option contrast-picker-option-high" data-contrast-preference-value="high" aria-label="Use high contrast" aria-pressed="false" disabled>
14
+ <button type="button" class="preference-picker-option contrast-picker-option-high" data-contrast-preference-value="high" aria-label="Use high contrast" aria-pressed="<%= site.defaultContrast === 'high' %>" disabled>
15
15
  <span class="material-symbol-icon material-symbol-icon-contrast-high" aria-hidden="true"></span>
16
16
  </button>
17
17
  </div>
@@ -1,5 +1,9 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="en"
3
+ data-default-font-preference="<%= site.defaultFont %>"
4
+ data-default-contrast-preference="<%= site.defaultContrast %>"<% if (site.defaultFont === 'serif') { %>
5
+ data-font-preference="serif"<% } %><% if (site.defaultContrast === 'high') { %>
6
+ data-contrast-preference="high"<% } %>>
3
7
  <head>
4
8
  <% if (site.features.favicon) { %>
5
9
  <link rel="icon" href="/favicon.ico" sizes="any" />
@@ -36,13 +40,33 @@
36
40
  <script>
37
41
  document.documentElement.classList.add('js');
38
42
  try {
39
- if (window.localStorage.getItem('fontPreference') === 'serif') {
40
- document.documentElement.dataset.fontPreference = 'serif';
43
+ const preference = window.localStorage.getItem('fontPreference');
44
+ const configured =
45
+ document.documentElement.dataset.defaultFontPreference;
46
+ if (
47
+ (preference === 'serif' || preference === 'sans') &&
48
+ preference !== configured
49
+ ) {
50
+ if (preference === 'serif') {
51
+ document.documentElement.dataset.fontPreference = 'serif';
52
+ } else {
53
+ delete document.documentElement.dataset.fontPreference;
54
+ }
41
55
  }
42
56
  } catch {}
43
57
  try {
44
- if (window.localStorage.getItem('contrastPreference') === 'high') {
45
- document.documentElement.dataset.contrastPreference = 'high';
58
+ const preference = window.localStorage.getItem('contrastPreference');
59
+ const configured =
60
+ document.documentElement.dataset.defaultContrastPreference;
61
+ if (
62
+ (preference === 'high' || preference === 'standard') &&
63
+ preference !== configured
64
+ ) {
65
+ if (preference === 'high') {
66
+ document.documentElement.dataset.contrastPreference = 'high';
67
+ } else {
68
+ delete document.documentElement.dataset.contrastPreference;
69
+ }
46
70
  }
47
71
  } catch {}
48
72
  </script>