@abreen/tada 1.16.0 → 1.16.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.
- package/README.md +19 -0
- package/assets/material-symbols/celebration_24px.svg +1 -1
- package/assets/material-symbols/close_24px.svg +1 -1
- package/assets/material-symbols/contrast_20px.svg +1 -0
- package/assets/material-symbols/contrast_rtl_off_20px.svg +1 -0
- package/assets/material-symbols/info_20px.svg +1 -0
- package/assets/material-symbols/info_40px.svg +1 -0
- package/assets/material-symbols/menu_24px.svg +1 -1
- package/assets/material-symbols/open_in_new_20px.svg +1 -0
- package/assets/material-symbols/search_20px.svg +1 -0
- package/assets/material-symbols/smart_display_24px.svg +1 -0
- package/assets/material-symbols/south_east_20px.svg +1 -0
- package/assets/material-symbols/warning_20px.svg +1 -0
- package/assets/material-symbols/warning_40px.svg +1 -0
- package/bin/validators.ts +2 -0
- package/build/bundle.ts +23 -4
- package/build/custom-fonts.ts +300 -0
- package/build/generate-fonts.ts +43 -0
- package/build/material-symbols.ts +58 -20
- package/build/pipeline.ts +1 -1
- package/build/site-variables.ts +2 -0
- package/build/types.d.ts +29 -0
- package/build/utils/render.ts +36 -12
- package/build/watch/assets.ts +3 -3
- package/build/watch/build-full.ts +1 -1
- package/build/watch/build-incremental.ts +1 -1
- package/build/watch/validation.ts +18 -7
- package/package.json +1 -1
- package/schema/site.schema.json +77 -0
- package/src/_alerts.scss +17 -12
- package/src/_layout.scss +2 -2
- package/src/anchor/index.ts +2 -35
- package/src/anchor/style.scss +4 -28
- package/src/appearance-picker/index.ts +78 -19
- package/src/appearance-picker/style.scss +9 -11
- package/src/header/_base.scss +14 -6
- package/src/header/style.scss +8 -0
- package/src/material-symbols.scss +16 -3
- package/src/navigate/runtime.ts +13 -0
- package/src/question/style.scss +1 -2
- package/src/search/style.scss +5 -3
- package/src/style.scss +11 -5
- package/src/toc/style.scss +14 -10
- package/templates/_appearance-picker.html +4 -4
- package/templates/_theme.scss +14 -4
- package/templates/_top.html +42 -6
- package/templates/code.html +1 -1
- package/assets/material-symbols/contrast_24px.svg +0 -1
- package/assets/material-symbols/contrast_rtl_off_24px.svg +0 -1
- package/assets/material-symbols/info_24px.svg +0 -1
- package/assets/material-symbols/north_east_24px.svg +0 -1
- package/assets/material-symbols/search_24px.svg +0 -1
- package/assets/material-symbols/south_east_24px.svg +0 -1
- package/assets/material-symbols/tag_24px.svg +0 -1
- package/assets/material-symbols/warning_24px.svg +0 -1
package/build/types.d.ts
CHANGED
|
@@ -3,6 +3,32 @@ import type { TadaProjectScan } from './source-model';
|
|
|
3
3
|
|
|
4
4
|
export type PlainTextLanguage = 'text' | 'txt' | 'plain';
|
|
5
5
|
|
|
6
|
+
export interface SerifFontTuning {
|
|
7
|
+
scale?: number;
|
|
8
|
+
lineHeight?: number;
|
|
9
|
+
headingScale?: number;
|
|
10
|
+
headingWeight?: 400 | 700;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SerifMonoFontTuning {
|
|
14
|
+
scale?: number;
|
|
15
|
+
lineHeight?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FontFamilyOverride<Tuning = unknown> {
|
|
19
|
+
regular: string;
|
|
20
|
+
italic?: string;
|
|
21
|
+
bold?: string;
|
|
22
|
+
boldItalic?: string;
|
|
23
|
+
features?: string[];
|
|
24
|
+
tuning?: Tuning;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FontOverrides {
|
|
28
|
+
serif?: FontFamilyOverride<SerifFontTuning>;
|
|
29
|
+
serifMono?: FontFamilyOverride<SerifMonoFontTuning>;
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
/** Site configuration loaded from site.dev.yaml/yml/json or site.prod.yaml/yml/json */
|
|
7
33
|
export interface SiteVariables {
|
|
8
34
|
base: string;
|
|
@@ -16,6 +42,9 @@ export interface SiteVariables {
|
|
|
16
42
|
faviconFontWeight?: number;
|
|
17
43
|
internalDomains?: string[];
|
|
18
44
|
defaultTimeZone: string;
|
|
45
|
+
defaultFont?: 'sans' | 'serif';
|
|
46
|
+
defaultContrast?: 'standard' | 'high';
|
|
47
|
+
fontOverrides?: FontOverrides;
|
|
19
48
|
features: FeatureConfig;
|
|
20
49
|
extensionToShikiLanguage?: Record<
|
|
21
50
|
string,
|
package/build/utils/render.ts
CHANGED
|
@@ -7,6 +7,8 @@ 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';
|
|
11
|
+
import { encodePublicAssetPath } from '../custom-fonts';
|
|
10
12
|
import { config } from '../templates';
|
|
11
13
|
import { render } from '../templates';
|
|
12
14
|
import {
|
|
@@ -157,6 +159,7 @@ export function injectAssetTags(
|
|
|
157
159
|
html: string,
|
|
158
160
|
assetFiles: string[],
|
|
159
161
|
distDir: string,
|
|
162
|
+
siteVariables: SiteVariables,
|
|
160
163
|
): string {
|
|
161
164
|
const jsAssets = assetFiles.filter(f => f.endsWith('.js'));
|
|
162
165
|
const cssAssets = assetFiles.filter(f => f.endsWith('.css'));
|
|
@@ -182,15 +185,19 @@ export function injectAssetTags(
|
|
|
182
185
|
)
|
|
183
186
|
.join('');
|
|
184
187
|
|
|
185
|
-
const fontPreloadTags =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.map(
|
|
191
|
-
f =>
|
|
192
|
-
`<link rel="preload" href="${normalizeOutputPath('/' + f)}" as="font" type="font/woff2" crossorigin>`,
|
|
188
|
+
const fontPreloadTags = getDefaultFontPreloadFiles(siteVariables)
|
|
189
|
+
.filter(
|
|
190
|
+
preload =>
|
|
191
|
+
preload.source === 'public' ||
|
|
192
|
+
fs.existsSync(path.join(distDir, preload.filePath)),
|
|
193
193
|
)
|
|
194
|
+
.map(preload => {
|
|
195
|
+
const urlPath =
|
|
196
|
+
preload.source === 'public'
|
|
197
|
+
? encodePublicAssetPath(preload.filePath)
|
|
198
|
+
: preload.filePath;
|
|
199
|
+
return `<link rel="preload" href="${normalizeOutputPath('/' + urlPath)}" as="font" type="font/woff2" crossorigin>`;
|
|
200
|
+
})
|
|
194
201
|
.join('');
|
|
195
202
|
|
|
196
203
|
return html
|
|
@@ -210,12 +217,14 @@ export function preparePageTemplateHtml({
|
|
|
210
217
|
templateHtml,
|
|
211
218
|
assetFiles,
|
|
212
219
|
distDir,
|
|
220
|
+
siteVariables,
|
|
213
221
|
}: {
|
|
214
222
|
templateHtml: string;
|
|
215
223
|
assetFiles: string[];
|
|
216
224
|
distDir: string;
|
|
225
|
+
siteVariables: SiteVariables;
|
|
217
226
|
}): string {
|
|
218
|
-
let html = injectAssetTags(templateHtml, assetFiles, distDir);
|
|
227
|
+
let html = injectAssetTags(templateHtml, assetFiles, distDir, siteVariables);
|
|
219
228
|
if (templateHtml.includes('class="katex"')) {
|
|
220
229
|
html = injectKatexStylesheet(html);
|
|
221
230
|
}
|
|
@@ -289,7 +298,12 @@ export function renderPlainTextPageAsset({
|
|
|
289
298
|
) as string;
|
|
290
299
|
const finalized = finalizeHtmlPage({
|
|
291
300
|
filePath,
|
|
292
|
-
html: preparePageTemplateHtml({
|
|
301
|
+
html: preparePageTemplateHtml({
|
|
302
|
+
templateHtml,
|
|
303
|
+
assetFiles,
|
|
304
|
+
distDir,
|
|
305
|
+
siteVariables,
|
|
306
|
+
}),
|
|
293
307
|
siteVariables,
|
|
294
308
|
sourceUrlPath,
|
|
295
309
|
validInternalTargets,
|
|
@@ -362,7 +376,12 @@ export function renderCodePageAsset({
|
|
|
362
376
|
const templateHtml = render('code.html', templateParameters) as string;
|
|
363
377
|
const finalized = finalizeHtmlPage({
|
|
364
378
|
filePath,
|
|
365
|
-
html: preparePageTemplateHtml({
|
|
379
|
+
html: preparePageTemplateHtml({
|
|
380
|
+
templateHtml,
|
|
381
|
+
assetFiles,
|
|
382
|
+
distDir,
|
|
383
|
+
siteVariables,
|
|
384
|
+
}),
|
|
366
385
|
siteVariables,
|
|
367
386
|
sourceUrlPath,
|
|
368
387
|
validInternalTargets,
|
|
@@ -700,7 +719,12 @@ export function renderLiterateJavaPageAsset({
|
|
|
700
719
|
const templateHtml = render('literate.html', templateParameters) as string;
|
|
701
720
|
const finalized = finalizeHtmlPage({
|
|
702
721
|
filePath,
|
|
703
|
-
html: preparePageTemplateHtml({
|
|
722
|
+
html: preparePageTemplateHtml({
|
|
723
|
+
templateHtml,
|
|
724
|
+
assetFiles,
|
|
725
|
+
distDir,
|
|
726
|
+
siteVariables,
|
|
727
|
+
}),
|
|
704
728
|
siteVariables,
|
|
705
729
|
sourceUrlPath,
|
|
706
730
|
validInternalTargets,
|
package/build/watch/assets.ts
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
75
|
+
...DEFAULT_FONT_PRELOAD_FILES.sans,
|
|
76
|
+
...DEFAULT_FONT_PRELOAD_FILES.serif,
|
|
77
77
|
];
|
|
78
78
|
|
|
79
79
|
for (const relPath of relPaths) {
|
|
@@ -32,7 +32,7 @@ export async function buildFull({
|
|
|
32
32
|
try {
|
|
33
33
|
const siteVariables = getDevSiteVariables();
|
|
34
34
|
const scan = scanProject(siteVariables);
|
|
35
|
-
const configDiagnostics = validateConfig(scan);
|
|
35
|
+
const configDiagnostics = validateConfig(scan, siteVariables);
|
|
36
36
|
if (configDiagnostics.length > 0) {
|
|
37
37
|
return buildFailedWithDiagnostics(outputDir, configDiagnostics);
|
|
38
38
|
}
|
|
@@ -34,7 +34,7 @@ export async function buildIncremental({
|
|
|
34
34
|
const outputDir = makeTempBuildDir(distDir);
|
|
35
35
|
try {
|
|
36
36
|
const siteVariables = snapshot.siteVariables;
|
|
37
|
-
const configDiagnostics = validateConfig(plan.scan);
|
|
37
|
+
const configDiagnostics = validateConfig(plan.scan, siteVariables);
|
|
38
38
|
if (configDiagnostics.length > 0) {
|
|
39
39
|
return buildFailedWithDiagnostics(outputDir, configDiagnostics);
|
|
40
40
|
}
|
|
@@ -2,6 +2,8 @@ import { B } from '../colors';
|
|
|
2
2
|
import { makeLogger } from '../log';
|
|
3
3
|
import { config, getConfigFileName } from '../templates';
|
|
4
4
|
import { validateConfigLinks } from '../validate-config-links';
|
|
5
|
+
import { validateCustomFontOverrides } from '../custom-fonts';
|
|
6
|
+
import type { SiteVariables } from '../types';
|
|
5
7
|
import type { WatchDiagnostic } from './types';
|
|
6
8
|
import { assertNoOutputPathConflicts, type TadaProjectScan } from './snapshot';
|
|
7
9
|
|
|
@@ -11,20 +13,29 @@ export function diagnosticsFromMessages(messages: string[]): WatchDiagnostic[] {
|
|
|
11
13
|
return messages.map(message => ({ message }));
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
export function validateConfig(
|
|
16
|
+
export function validateConfig(
|
|
17
|
+
scan: TadaProjectScan,
|
|
18
|
+
siteVariables: SiteVariables,
|
|
19
|
+
): WatchDiagnostic[] {
|
|
20
|
+
const diagnostics = diagnosticsFromMessages(
|
|
21
|
+
validateCustomFontOverrides({
|
|
22
|
+
fontOverrides: siteVariables.fontOverrides,
|
|
23
|
+
publicDir: scan.publicDir,
|
|
24
|
+
publicFiles: scan.publicFiles,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
15
27
|
const conflicts = assertNoOutputPathConflicts(scan);
|
|
16
28
|
if (conflicts.length === 0) {
|
|
17
|
-
return
|
|
29
|
+
return diagnostics;
|
|
18
30
|
}
|
|
19
31
|
for (const relPath of conflicts) {
|
|
20
32
|
log.error`content/${B`${relPath}`} conflicts with public/${B`${relPath}`}`;
|
|
21
33
|
}
|
|
22
34
|
const noun = conflicts.length === 1 ? 'file' : 'files';
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
];
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
message: `${conflicts.length} ${noun} in content/ and public/ have the same path`,
|
|
37
|
+
});
|
|
38
|
+
return diagnostics;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
export function validateProjectConfigLinks(
|
package/package.json
CHANGED
package/schema/site.schema.json
CHANGED
|
@@ -48,6 +48,24 @@
|
|
|
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
|
+
},
|
|
61
|
+
"fontOverrides": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"serif": { "$ref": "#/$defs/serifFontOverride" },
|
|
65
|
+
"serifMono": { "$ref": "#/$defs/serifMonoFontOverride" }
|
|
66
|
+
},
|
|
67
|
+
"additionalProperties": false
|
|
68
|
+
},
|
|
51
69
|
"vars": { "type": "object" },
|
|
52
70
|
"extensionToShikiLanguage": {
|
|
53
71
|
"type": "object",
|
|
@@ -55,5 +73,64 @@
|
|
|
55
73
|
},
|
|
56
74
|
"shikiLanguages": { "type": "array", "items": { "type": "string" } }
|
|
57
75
|
},
|
|
76
|
+
"$defs": {
|
|
77
|
+
"serifFontOverride": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["regular"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"regular": { "$ref": "#/$defs/publicWoff2Path" },
|
|
82
|
+
"italic": { "$ref": "#/$defs/publicWoff2Path" },
|
|
83
|
+
"bold": { "$ref": "#/$defs/publicWoff2Path" },
|
|
84
|
+
"boldItalic": { "$ref": "#/$defs/publicWoff2Path" },
|
|
85
|
+
"features": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "type": "string", "pattern": "^[A-Za-z0-9]{4}$" },
|
|
88
|
+
"uniqueItems": true
|
|
89
|
+
},
|
|
90
|
+
"tuning": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"scale": { "$ref": "#/$defs/fontScale" },
|
|
94
|
+
"lineHeight": { "$ref": "#/$defs/fontLineHeight" },
|
|
95
|
+
"headingScale": { "$ref": "#/$defs/fontScale" },
|
|
96
|
+
"headingWeight": { "type": "integer", "enum": [400, 700] }
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": false
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"additionalProperties": false
|
|
102
|
+
},
|
|
103
|
+
"serifMonoFontOverride": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": ["regular"],
|
|
106
|
+
"properties": {
|
|
107
|
+
"regular": { "$ref": "#/$defs/publicWoff2Path" },
|
|
108
|
+
"italic": { "$ref": "#/$defs/publicWoff2Path" },
|
|
109
|
+
"bold": { "$ref": "#/$defs/publicWoff2Path" },
|
|
110
|
+
"boldItalic": { "$ref": "#/$defs/publicWoff2Path" },
|
|
111
|
+
"features": {
|
|
112
|
+
"type": "array",
|
|
113
|
+
"items": { "type": "string", "pattern": "^[A-Za-z0-9]{4}$" },
|
|
114
|
+
"uniqueItems": true
|
|
115
|
+
},
|
|
116
|
+
"tuning": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"scale": { "$ref": "#/$defs/fontScale" },
|
|
120
|
+
"lineHeight": { "$ref": "#/$defs/fontLineHeight" }
|
|
121
|
+
},
|
|
122
|
+
"additionalProperties": false
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"additionalProperties": false
|
|
126
|
+
},
|
|
127
|
+
"fontScale": { "type": "number", "minimum": 0.75, "maximum": 1.5 },
|
|
128
|
+
"fontLineHeight": { "type": "number", "minimum": 1, "maximum": 2.5 },
|
|
129
|
+
"publicWoff2Path": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"minLength": 6,
|
|
132
|
+
"pattern": "\\.woff2$"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
58
135
|
"additionalProperties": false
|
|
59
136
|
}
|
package/src/_alerts.scss
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
.alert .title {
|
|
20
20
|
padding: 10px 6px;
|
|
21
|
+
padding-top: 18px;
|
|
22
|
+
padding-left: 58px;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
.alert .content {
|
|
@@ -67,32 +69,35 @@
|
|
|
67
69
|
|
|
68
70
|
.alert .title::before {
|
|
69
71
|
position: absolute;
|
|
70
|
-
top:
|
|
71
|
-
left:
|
|
72
|
-
width:
|
|
73
|
-
height:
|
|
72
|
+
top: 12px;
|
|
73
|
+
left: 10px;
|
|
74
|
+
width: var(--alert-icon-size);
|
|
75
|
+
height: var(--alert-icon-size);
|
|
74
76
|
content: '';
|
|
77
|
+
|
|
78
|
+
@include contained-mask(var(--alert-icon));
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
.alert.warning
|
|
78
|
-
|
|
81
|
+
.alert.warning {
|
|
82
|
+
--alert-icon: var(--icon-warning);
|
|
83
|
+
--alert-icon-size: var(--icon-warning-size);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
i.warning {
|
|
82
|
-
@include contained-mask(var(--icon-warning));
|
|
87
|
+
@include contained-mask(var(--icon-warning-compact));
|
|
83
88
|
|
|
84
89
|
display: inline-block;
|
|
85
|
-
width:
|
|
86
|
-
height:
|
|
90
|
+
width: var(--icon-warning-compact-size);
|
|
91
|
+
height: var(--icon-warning-compact-size);
|
|
87
92
|
vertical-align: sub;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
.alert.note
|
|
91
|
-
|
|
95
|
+
.alert.note {
|
|
96
|
+
--alert-icon: var(--icon-info);
|
|
97
|
+
--alert-icon-size: var(--icon-info-size);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
.alert .title {
|
|
95
|
-
padding-left: 48px;
|
|
96
101
|
margin: 0;
|
|
97
102
|
font-weight: 700;
|
|
98
103
|
|
package/src/_layout.scss
CHANGED
package/src/anchor/index.ts
CHANGED
|
@@ -2,45 +2,13 @@ function getElements(parent: HTMLElement): HTMLHeadingElement[] {
|
|
|
2
2
|
return Array.from(parent.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
function
|
|
5
|
+
function createPresentIcon(window: Window): HTMLSpanElement {
|
|
6
6
|
const icon = window.document.createElement('span');
|
|
7
|
-
icon.className = 'material-symbol-icon material-symbol-icon-heading-
|
|
7
|
+
icon.className = 'material-symbol-icon material-symbol-icon-heading-present';
|
|
8
8
|
icon.setAttribute('aria-hidden', 'true');
|
|
9
9
|
return icon;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function createPresentIcon(window: Window): SVGSVGElement {
|
|
13
|
-
const svg = window.document.createElementNS(
|
|
14
|
-
'http://www.w3.org/2000/svg',
|
|
15
|
-
'svg',
|
|
16
|
-
);
|
|
17
|
-
svg.setAttribute('aria-hidden', 'true');
|
|
18
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
19
|
-
svg.setAttribute('width', '16');
|
|
20
|
-
svg.setAttribute('height', '16');
|
|
21
|
-
svg.setAttribute('fill', 'none');
|
|
22
|
-
svg.setAttribute('stroke', 'currentColor');
|
|
23
|
-
svg.setAttribute('stroke-width', '1.5');
|
|
24
|
-
svg.setAttribute('stroke-linecap', 'round');
|
|
25
|
-
svg.setAttribute('stroke-linejoin', 'round');
|
|
26
|
-
|
|
27
|
-
const paths = [
|
|
28
|
-
'M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z',
|
|
29
|
-
'M9 20h6',
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
for (const d of paths) {
|
|
33
|
-
const path = window.document.createElementNS(
|
|
34
|
-
'http://www.w3.org/2000/svg',
|
|
35
|
-
'path',
|
|
36
|
-
);
|
|
37
|
-
path.setAttribute('d', d);
|
|
38
|
-
svg.appendChild(path);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return svg;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
12
|
function getSlideIndex(slide: HTMLElement): number {
|
|
45
13
|
const rawIndex = slide.getAttribute('data-slide-index');
|
|
46
14
|
if (rawIndex != null) {
|
|
@@ -86,7 +54,6 @@ export default (window: Window) => {
|
|
|
86
54
|
while (el.firstChild) {
|
|
87
55
|
link.appendChild(el.firstChild);
|
|
88
56
|
}
|
|
89
|
-
link.appendChild(createHeadingAnchorIcon(window));
|
|
90
57
|
el.appendChild(link);
|
|
91
58
|
|
|
92
59
|
const handleClick = () => {
|
package/src/anchor/style.scss
CHANGED
|
@@ -7,28 +7,12 @@ a.heading-anchor {
|
|
|
7
7
|
@include subdued-link;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.heading-anchor .material-symbol-icon,
|
|
11
|
-
.heading-present-button svg {
|
|
12
|
-
display: inline-block;
|
|
13
|
-
width: 1em;
|
|
14
|
-
height: 1em;
|
|
15
|
-
vertical-align: middle;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.heading-anchor .material-symbol-icon {
|
|
19
|
-
margin-left: 4px;
|
|
20
|
-
vertical-align: middle;
|
|
21
|
-
opacity: 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
10
|
.heading-present-button {
|
|
25
|
-
display: inline-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
width: 1em;
|
|
29
|
-
height: 1em;
|
|
11
|
+
display: inline-block;
|
|
12
|
+
width: var(--icon-heading-present-size);
|
|
13
|
+
height: var(--icon-heading-present-size);
|
|
30
14
|
padding: 0;
|
|
31
|
-
margin-left:
|
|
15
|
+
margin-left: 8px;
|
|
32
16
|
font: inherit;
|
|
33
17
|
line-height: 1;
|
|
34
18
|
vertical-align: middle;
|
|
@@ -39,7 +23,6 @@ a.heading-anchor {
|
|
|
39
23
|
opacity: 0;
|
|
40
24
|
}
|
|
41
25
|
|
|
42
|
-
a.heading-anchor:focus-visible .material-symbol-icon,
|
|
43
26
|
.heading-present-button:focus {
|
|
44
27
|
opacity: 1;
|
|
45
28
|
}
|
|
@@ -65,12 +48,6 @@ body.is-presenting .heading-present-button {
|
|
|
65
48
|
background: transparent;
|
|
66
49
|
}
|
|
67
50
|
|
|
68
|
-
h1:hover a.heading-anchor .material-symbol-icon,
|
|
69
|
-
h2:hover a.heading-anchor .material-symbol-icon,
|
|
70
|
-
h3:hover a.heading-anchor .material-symbol-icon,
|
|
71
|
-
h4:hover a.heading-anchor .material-symbol-icon,
|
|
72
|
-
h5:hover a.heading-anchor .material-symbol-icon,
|
|
73
|
-
h6:hover a.heading-anchor .material-symbol-icon,
|
|
74
51
|
h1:hover .heading-present-button,
|
|
75
52
|
h2:hover .heading-present-button,
|
|
76
53
|
h3:hover .heading-present-button,
|
|
@@ -88,7 +65,6 @@ body.is-presenting .heading-present-button {
|
|
|
88
65
|
}
|
|
89
66
|
|
|
90
67
|
@media print {
|
|
91
|
-
.heading-anchor .material-symbol-icon,
|
|
92
68
|
.heading-present-button {
|
|
93
69
|
display: none;
|
|
94
70
|
}
|
|
@@ -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
|
-
|
|
18
|
+
storedValues: readonly T[],
|
|
19
19
|
defaultValue: T,
|
|
20
20
|
): T {
|
|
21
21
|
try {
|
|
22
|
-
|
|
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,82 @@ function saveStoredPreference<T extends string>(
|
|
|
29
32
|
storage: Storage | null,
|
|
30
33
|
key: string,
|
|
31
34
|
preference: T,
|
|
32
|
-
|
|
35
|
+
defaultValue: T,
|
|
33
36
|
): void {
|
|
34
37
|
try {
|
|
35
|
-
if (preference ===
|
|
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(
|
|
46
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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(window: Window): ContrastPreference {
|
|
95
|
+
const configured =
|
|
96
|
+
window.document.documentElement.dataset.defaultContrastPreference === 'high'
|
|
97
|
+
? 'high'
|
|
98
|
+
: 'standard';
|
|
99
|
+
|
|
100
|
+
if (configured === 'high' || typeof window.matchMedia !== 'function') {
|
|
101
|
+
return configured;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
return window.matchMedia('(prefers-contrast: more)').matches
|
|
106
|
+
? 'high'
|
|
107
|
+
: configured;
|
|
108
|
+
} catch {
|
|
109
|
+
return configured;
|
|
110
|
+
}
|
|
67
111
|
}
|
|
68
112
|
|
|
69
113
|
export function applyFontPreference(
|
|
@@ -71,8 +115,10 @@ export function applyFontPreference(
|
|
|
71
115
|
preference: FontPreference,
|
|
72
116
|
): void {
|
|
73
117
|
if (preference === 'serif') {
|
|
74
|
-
document.documentElement.dataset.fontPreference
|
|
75
|
-
|
|
118
|
+
if (document.documentElement.dataset.fontPreference !== preference) {
|
|
119
|
+
document.documentElement.dataset.fontPreference = preference;
|
|
120
|
+
}
|
|
121
|
+
} else if (document.documentElement.dataset.fontPreference !== undefined) {
|
|
76
122
|
delete document.documentElement.dataset.fontPreference;
|
|
77
123
|
}
|
|
78
124
|
}
|
|
@@ -82,8 +128,12 @@ export function applyContrastPreference(
|
|
|
82
128
|
preference: ContrastPreference,
|
|
83
129
|
): void {
|
|
84
130
|
if (preference === 'high') {
|
|
85
|
-
document.documentElement.dataset.contrastPreference
|
|
86
|
-
|
|
131
|
+
if (document.documentElement.dataset.contrastPreference !== preference) {
|
|
132
|
+
document.documentElement.dataset.contrastPreference = preference;
|
|
133
|
+
}
|
|
134
|
+
} else if (
|
|
135
|
+
document.documentElement.dataset.contrastPreference !== undefined
|
|
136
|
+
) {
|
|
87
137
|
delete document.documentElement.dataset.contrastPreference;
|
|
88
138
|
}
|
|
89
139
|
}
|
|
@@ -129,8 +179,13 @@ export default function mountAppearancePicker(window: Window): () => void {
|
|
|
129
179
|
container.querySelectorAll<HTMLButtonElement>('button'),
|
|
130
180
|
);
|
|
131
181
|
const storage = getStorage(window);
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
const defaultFontPreference = getDefaultFontPreference(document);
|
|
183
|
+
const defaultContrastPreference = getDefaultContrastPreference(window);
|
|
184
|
+
let fontPreference = getFontPreference(storage, defaultFontPreference);
|
|
185
|
+
let contrastPreference = getContrastPreference(
|
|
186
|
+
storage,
|
|
187
|
+
defaultContrastPreference,
|
|
188
|
+
);
|
|
134
189
|
|
|
135
190
|
applyFontPreference(document, fontPreference);
|
|
136
191
|
applyContrastPreference(document, contrastPreference);
|
|
@@ -143,11 +198,15 @@ export default function mountAppearancePicker(window: Window): () => void {
|
|
|
143
198
|
|
|
144
199
|
if (fontValue === 'sans' || fontValue === 'serif') {
|
|
145
200
|
fontPreference = fontValue;
|
|
146
|
-
saveFontPreference(storage, fontPreference);
|
|
201
|
+
saveFontPreference(storage, fontPreference, defaultFontPreference);
|
|
147
202
|
applyFontPreference(document, fontPreference);
|
|
148
203
|
} else if (contrastValue === 'standard' || contrastValue === 'high') {
|
|
149
204
|
contrastPreference = contrastValue;
|
|
150
|
-
saveContrastPreference(
|
|
205
|
+
saveContrastPreference(
|
|
206
|
+
storage,
|
|
207
|
+
contrastPreference,
|
|
208
|
+
defaultContrastPreference,
|
|
209
|
+
);
|
|
151
210
|
applyContrastPreference(document, contrastPreference);
|
|
152
211
|
} else {
|
|
153
212
|
return;
|