@abreen/tada 1.1.0 → 1.3.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.
Files changed (94) hide show
  1. package/README.md +69 -17
  2. package/bin/tada.ts +317 -25
  3. package/bin/validators.test.ts +4 -3
  4. package/bin/validators.ts +4 -2
  5. package/build/apply-base-path-plugin.ts +24 -13
  6. package/build/build-manifest.test.ts +349 -0
  7. package/build/build-manifest.ts +165 -0
  8. package/build/bundle.ts +19 -20
  9. package/build/code.test.ts +7 -3
  10. package/build/content-watch.ts +6 -6
  11. package/build/copy.ts +2 -2
  12. package/build/deflist-id-plugin.ts +1 -1
  13. package/build/external-links-plugin.ts +2 -2
  14. package/build/features.ts +1 -1
  15. package/build/generate-content-assets.ts +12 -7
  16. package/build/generate-favicon.ts +13 -6
  17. package/build/generate-fonts.ts +2 -2
  18. package/build/generate-katex-assets.ts +35 -0
  19. package/build/generate-manifest.ts +2 -2
  20. package/build/globals.test.ts +10 -11
  21. package/build/globals.ts +14 -7
  22. package/build/json-schema.test.ts +1 -1
  23. package/build/log.test.ts +1 -1
  24. package/build/log.ts +2 -2
  25. package/build/markdown-plugins.test.ts +116 -10
  26. package/build/pagefind.test.ts +2 -2
  27. package/build/pagefind.ts +5 -5
  28. package/build/pdf-text.test.ts +52 -0
  29. package/build/pipeline.ts +42 -15
  30. package/build/reachability.test.ts +1 -1
  31. package/build/serve.ts +20 -8
  32. package/build/site-variables.ts +13 -5
  33. package/build/templates.ts +16 -7
  34. package/build/toc-plugin.test.ts +2 -2
  35. package/build/toc-plugin.ts +1 -1
  36. package/build/types.ts +15 -6
  37. package/build/util.ts +6 -6
  38. package/build/utils/code.ts +5 -5
  39. package/build/utils/content-files.ts +3 -3
  40. package/build/utils/derive-theme.test.ts +1 -1
  41. package/build/utils/derive-theme.ts +1 -1
  42. package/build/utils/file-types.test.ts +1 -1
  43. package/build/utils/front-matter.test.ts +1 -4
  44. package/build/utils/front-matter.ts +2 -2
  45. package/build/utils/jdi-runner/LiterateRunner.java +13 -0
  46. package/build/utils/literate-java.ts +24 -12
  47. package/build/utils/markdown.ts +41 -9
  48. package/build/utils/paths.test.ts +2 -2
  49. package/build/utils/paths.ts +5 -1
  50. package/build/utils/render.ts +57 -36
  51. package/build/utils/shiki-highlighter.ts +1 -1
  52. package/build/validate-internal-links-plugin.test.ts +1 -1
  53. package/build/validate-internal-links-plugin.ts +1 -1
  54. package/build/watch-reachability-state.test.ts +2 -2
  55. package/build/watch-reachability-state.ts +1 -1
  56. package/build/watch.ts +91 -110
  57. package/init/content/labs/00/VowelCounter.java.md +129 -0
  58. package/init/content/labs/00/index.md +29 -0
  59. package/init/content/labs/index.md +9 -0
  60. package/{content → init/content}/lectures/01/Pair.java.md +1 -0
  61. package/{content → init/content}/lectures/index.md +1 -1
  62. package/{content → init/content}/markdown.md +26 -7
  63. package/package.json +19 -7
  64. package/{templates → schema}/nav.schema.json +1 -2
  65. package/{build → schema}/site.schema.json +3 -5
  66. package/src/_alerts.scss +4 -0
  67. package/src/_base.scss +90 -6
  68. package/src/_layout.scss +39 -2
  69. package/src/anchor/index.ts +0 -1
  70. package/src/index.ts +3 -3
  71. package/src/question/style.scss +8 -3
  72. package/src/search/index.test.ts +203 -0
  73. package/src/search/index.ts +3 -79
  74. package/src/search/pdf-utils.ts +79 -0
  75. package/src/search/style.scss +2 -20
  76. package/src/style.scss +20 -101
  77. package/src/timezone/index.ts +9 -25
  78. package/src/util.ts +1 -1
  79. package/templates/_top.html +2 -1
  80. package/types/dev.d.ts +1 -3
  81. package/types/site-variables.d.ts +6 -10
  82. package/types/untyped-modules.d.ts +7 -0
  83. package/build/utils/jdi-runner/LiterateRunner.class +0 -0
  84. package/config/authors.json +0 -1
  85. package/config/nav.json +0 -28
  86. /package/{content → init/content}/index.md +0 -0
  87. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  88. /package/{content → init/content}/lectures/01/demo.py +0 -0
  89. /package/{content → init/content}/lectures/01/index.md +0 -0
  90. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  91. /package/{content → init/content}/problem_sets/index.html +0 -0
  92. /package/{public → init/public}/avatars/alex.jpg +0 -0
  93. /package/{public → init/public}/test.txt +0 -0
  94. /package/{templates → schema}/authors.schema.json +0 -0
@@ -0,0 +1,203 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getPdfPageNumber, getPdfBaseUrl, groupPdfResults } from './pdf-utils';
3
+ import type { Result } from './pdf-utils';
4
+
5
+ describe('getPdfPageNumber', () => {
6
+ test('returns page number from meta string', () => {
7
+ expect(getPdfPageNumber('/doc.pdf#page=3', '3')).toBe(3);
8
+ });
9
+
10
+ test('prefers meta over URL hash', () => {
11
+ expect(getPdfPageNumber('/doc.pdf#page=5', '2')).toBe(2);
12
+ });
13
+
14
+ test('falls back to URL hash when meta is undefined', () => {
15
+ expect(getPdfPageNumber('/doc.pdf#page=7', undefined)).toBe(7);
16
+ });
17
+
18
+ test('falls back to URL hash when meta is empty', () => {
19
+ expect(getPdfPageNumber('/doc.pdf#page=4', '')).toBe(4);
20
+ });
21
+
22
+ test('falls back to URL hash when meta is not a number', () => {
23
+ expect(getPdfPageNumber('/doc.pdf#page=4', 'abc')).toBe(4);
24
+ });
25
+
26
+ test('returns null when neither meta nor hash has a page', () => {
27
+ expect(getPdfPageNumber('/doc.pdf', undefined)).toBeNull();
28
+ });
29
+
30
+ test('returns null for non-positive meta page number', () => {
31
+ expect(getPdfPageNumber('/doc.pdf', '0')).toBeNull();
32
+ });
33
+
34
+ test('returns null for negative meta page number', () => {
35
+ expect(getPdfPageNumber('/doc.pdf', '-1')).toBeNull();
36
+ });
37
+
38
+ test('returns null for hash page=0', () => {
39
+ expect(getPdfPageNumber('/doc.pdf#page=0', undefined)).toBeNull();
40
+ });
41
+
42
+ test('parses page from hash with other params', () => {
43
+ expect(getPdfPageNumber('/doc.pdf#zoom=100&page=12', undefined)).toBe(12);
44
+ });
45
+
46
+ test('handles case-insensitive page hash', () => {
47
+ expect(getPdfPageNumber('/doc.pdf#Page=9', undefined)).toBe(9);
48
+ });
49
+ });
50
+
51
+ describe('getPdfBaseUrl', () => {
52
+ test('returns base URL for a PDF with hash', () => {
53
+ expect(getPdfBaseUrl('/docs/guide.pdf#page=3')).toBe('/docs/guide.pdf');
54
+ });
55
+
56
+ test('returns URL unchanged when no hash present', () => {
57
+ expect(getPdfBaseUrl('/docs/guide.pdf')).toBe('/docs/guide.pdf');
58
+ });
59
+
60
+ test('returns null for non-PDF URL', () => {
61
+ expect(getPdfBaseUrl('/about/')).toBeNull();
62
+ });
63
+
64
+ test('returns null for non-PDF URL with hash', () => {
65
+ expect(getPdfBaseUrl('/about/#section')).toBeNull();
66
+ });
67
+
68
+ test('handles .PDF extension case-insensitively', () => {
69
+ expect(getPdfBaseUrl('/docs/GUIDE.PDF#page=1')).toBe('/docs/GUIDE.PDF');
70
+ });
71
+ });
72
+
73
+ describe('groupPdfResults', () => {
74
+ function makeResult(overrides: Partial<Result>): Result {
75
+ return {
76
+ title: 'doc.pdf',
77
+ url: '/doc.pdf',
78
+ excerpt: '',
79
+ score: 1,
80
+ subResults: [],
81
+ pageNumber: null,
82
+ ...overrides,
83
+ };
84
+ }
85
+
86
+ test('passes through non-PDF results unchanged', () => {
87
+ const results: Result[] = [
88
+ makeResult({ title: 'About', url: '/about/', score: 5 }),
89
+ makeResult({ title: 'Home', url: '/', score: 3 }),
90
+ ];
91
+ const grouped = groupPdfResults(results);
92
+ expect(grouped).toEqual(results);
93
+ });
94
+
95
+ test('groups multiple PDF page results into one result with sub-results', () => {
96
+ const results: Result[] = [
97
+ makeResult({
98
+ url: '/doc.pdf#page=3',
99
+ score: 10,
100
+ pageNumber: 3,
101
+ excerpt: 'page 3 text',
102
+ }),
103
+ makeResult({
104
+ url: '/doc.pdf#page=1',
105
+ score: 5,
106
+ pageNumber: 1,
107
+ excerpt: 'page 1 text',
108
+ }),
109
+ ];
110
+ const grouped = groupPdfResults(results);
111
+ expect(grouped).toHaveLength(1);
112
+ expect(grouped[0].url).toBe('/doc.pdf');
113
+ expect(grouped[0].score).toBe(10);
114
+ expect(grouped[0].subResults).toEqual([
115
+ { title: 'Page 1', url: '/doc.pdf#page=1', excerpt: 'page 1 text' },
116
+ { title: 'Page 3', url: '/doc.pdf#page=3', excerpt: 'page 3 text' },
117
+ ]);
118
+ });
119
+
120
+ test('sub-results are sorted by page number', () => {
121
+ const results: Result[] = [
122
+ makeResult({ url: '/doc.pdf#page=5', score: 1, pageNumber: 5 }),
123
+ makeResult({ url: '/doc.pdf#page=2', score: 8, pageNumber: 2 }),
124
+ makeResult({ url: '/doc.pdf#page=9', score: 3, pageNumber: 9 }),
125
+ ];
126
+ const grouped = groupPdfResults(results);
127
+ expect(grouped[0].subResults.map(s => s.title)).toEqual([
128
+ 'Page 2',
129
+ 'Page 5',
130
+ 'Page 9',
131
+ ]);
132
+ });
133
+
134
+ test('primary result uses highest-scoring page', () => {
135
+ const results: Result[] = [
136
+ makeResult({
137
+ url: '/doc.pdf#page=1',
138
+ score: 2,
139
+ pageNumber: 1,
140
+ excerpt: 'low',
141
+ }),
142
+ makeResult({
143
+ url: '/doc.pdf#page=4',
144
+ score: 9,
145
+ pageNumber: 4,
146
+ excerpt: 'high',
147
+ }),
148
+ ];
149
+ const grouped = groupPdfResults(results);
150
+ expect(grouped[0].excerpt).toBe('high');
151
+ expect(grouped[0].score).toBe(9);
152
+ });
153
+
154
+ test('mixes non-PDF and grouped PDF results sorted by score', () => {
155
+ const results: Result[] = [
156
+ makeResult({
157
+ title: 'About',
158
+ url: '/about/',
159
+ score: 7,
160
+ pageNumber: null,
161
+ }),
162
+ makeResult({ url: '/doc.pdf#page=1', score: 10, pageNumber: 1 }),
163
+ makeResult({ url: '/doc.pdf#page=2', score: 3, pageNumber: 2 }),
164
+ ];
165
+ const grouped = groupPdfResults(results);
166
+ expect(grouped).toHaveLength(2);
167
+ expect(grouped[0].url).toBe('/doc.pdf');
168
+ expect(grouped[0].score).toBe(10);
169
+ expect(grouped[1].url).toBe('/about/');
170
+ expect(grouped[1].score).toBe(7);
171
+ });
172
+
173
+ test('does not group PDF result without a page number', () => {
174
+ const results: Result[] = [
175
+ makeResult({ url: '/doc.pdf', score: 5, pageNumber: null }),
176
+ ];
177
+ const grouped = groupPdfResults(results);
178
+ expect(grouped).toHaveLength(1);
179
+ expect(grouped[0].url).toBe('/doc.pdf');
180
+ expect(grouped[0].subResults).toEqual([]);
181
+ });
182
+
183
+ test('groups results from different PDFs separately', () => {
184
+ const results: Result[] = [
185
+ makeResult({
186
+ title: 'a.pdf',
187
+ url: '/a.pdf#page=1',
188
+ score: 5,
189
+ pageNumber: 1,
190
+ }),
191
+ makeResult({
192
+ title: 'b.pdf',
193
+ url: '/b.pdf#page=1',
194
+ score: 8,
195
+ pageNumber: 1,
196
+ }),
197
+ ];
198
+ const grouped = groupPdfResults(results);
199
+ expect(grouped).toHaveLength(2);
200
+ const urls = grouped.map(r => r.url).sort();
201
+ expect(urls).toEqual(['/a.pdf', '/b.pdf']);
202
+ });
203
+ });
@@ -1,4 +1,6 @@
1
1
  import { getElement, applyBasePath } from '../util';
2
+ import { getPdfPageNumber, groupPdfResults } from './pdf-utils';
3
+ import type { Result } from './pdf-utils';
2
4
 
3
5
  const MAX_RESULTS = 24;
4
6
 
@@ -28,14 +30,6 @@ interface Pagefind {
28
30
  let pagefind: Pagefind | null = null;
29
31
 
30
32
  type SubResult = { title: string; url: string; excerpt: string };
31
- type Result = {
32
- title: string;
33
- url: string;
34
- excerpt: string;
35
- score: number;
36
- subResults: SubResult[];
37
- pageNumber: number | null;
38
- };
39
33
 
40
34
  type State = {
41
35
  value: string;
@@ -44,76 +38,6 @@ type State = {
44
38
  totalResults: number;
45
39
  };
46
40
 
47
- function getPdfPageNumber(
48
- url: string,
49
- pageMeta: string | undefined,
50
- ): number | null {
51
- const fromMeta = Number.parseInt(pageMeta ?? '', 10);
52
- if (Number.isInteger(fromMeta) && fromMeta > 0) {
53
- return fromMeta;
54
- }
55
- const match = url.match(/#(?:.*&)?page=(\d+)\b/i);
56
- if (!match) {
57
- return null;
58
- }
59
- const fromUrl = Number.parseInt(match[1], 10);
60
- return Number.isInteger(fromUrl) && fromUrl >= 1 ? fromUrl : null;
61
- }
62
-
63
- function getPdfBaseUrl(url: string): string | null {
64
- const hashIndex = url.indexOf('#');
65
- const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
66
- return baseUrl.toLowerCase().endsWith('.pdf') ? baseUrl : null;
67
- }
68
-
69
- function groupPdfResults(results: Result[]): Result[] {
70
- const pdfGroups = new Map<string, Result[]>();
71
- const other: Result[] = [];
72
-
73
- for (const result of results) {
74
- const baseUrl = getPdfBaseUrl(result.url);
75
- if (!baseUrl || result.pageNumber == null) {
76
- other.push(result);
77
- continue;
78
- }
79
- if (!pdfGroups.has(baseUrl)) {
80
- pdfGroups.set(baseUrl, []);
81
- }
82
- pdfGroups.get(baseUrl)!.push(result);
83
- }
84
-
85
- const grouped: Result[] = [...other];
86
-
87
- pdfGroups.forEach((pages, baseUrl) => {
88
- const primary = pages
89
- .slice()
90
- .sort(
91
- (a, b) =>
92
- b.score - a.score || (a.pageNumber ?? 0) - (b.pageNumber ?? 0),
93
- )[0];
94
- const subResults = pages
95
- .slice()
96
- .sort((a, b) => (a.pageNumber ?? 0) - (b.pageNumber ?? 0))
97
- .map(page => ({
98
- title: `Page ${page.pageNumber ?? '?'}`,
99
- url: page.url,
100
- excerpt: page.excerpt,
101
- }));
102
-
103
- grouped.push({
104
- title: primary.title,
105
- url: baseUrl,
106
- excerpt: primary.excerpt,
107
- score: primary.score,
108
- subResults,
109
- pageNumber: null,
110
- });
111
- });
112
-
113
- grouped.sort((a, b) => b.score - a.score);
114
- return grouped;
115
- }
116
-
117
41
  async function doSearch(state: State) {
118
42
  if (pagefind == null) {
119
43
  return;
@@ -129,7 +53,7 @@ async function doSearch(state: State) {
129
53
  const slice = search.results.slice(0, MAX_RESULTS);
130
54
  const data = await Promise.all(slice.map(r => r.data()));
131
55
 
132
- const titlePostfix = window.siteVariables.titlePostfix ?? '';
56
+ const titlePostfix = __SITE_TITLE_POSTFIX__;
133
57
  const results: Result[] = data.map((d: PagefindResult) => {
134
58
  let title: string = d.meta?.title ?? d.url;
135
59
  if (titlePostfix && title.endsWith(titlePostfix)) {
@@ -0,0 +1,79 @@
1
+ export type SubResult = { title: string; url: string; excerpt: string };
2
+ export type Result = {
3
+ title: string;
4
+ url: string;
5
+ excerpt: string;
6
+ score: number;
7
+ subResults: SubResult[];
8
+ pageNumber: number | null;
9
+ };
10
+
11
+ export function getPdfPageNumber(
12
+ url: string,
13
+ pageMeta: string | undefined,
14
+ ): number | null {
15
+ const fromMeta = Number.parseInt(pageMeta ?? '', 10);
16
+ if (Number.isInteger(fromMeta) && fromMeta > 0) {
17
+ return fromMeta;
18
+ }
19
+ const match = url.match(/#(?:.*&)?page=(\d+)\b/i);
20
+ if (!match) {
21
+ return null;
22
+ }
23
+ const fromUrl = Number.parseInt(match[1], 10);
24
+ return Number.isInteger(fromUrl) && fromUrl >= 1 ? fromUrl : null;
25
+ }
26
+
27
+ export function getPdfBaseUrl(url: string): string | null {
28
+ const hashIndex = url.indexOf('#');
29
+ const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
30
+ return baseUrl.toLowerCase().endsWith('.pdf') ? baseUrl : null;
31
+ }
32
+
33
+ export function groupPdfResults(results: Result[]): Result[] {
34
+ const pdfGroups = new Map<string, Result[]>();
35
+ const other: Result[] = [];
36
+
37
+ for (const result of results) {
38
+ const baseUrl = getPdfBaseUrl(result.url);
39
+ if (!baseUrl || result.pageNumber == null) {
40
+ other.push(result);
41
+ continue;
42
+ }
43
+ if (!pdfGroups.has(baseUrl)) {
44
+ pdfGroups.set(baseUrl, []);
45
+ }
46
+ pdfGroups.get(baseUrl)!.push(result);
47
+ }
48
+
49
+ const grouped: Result[] = [...other];
50
+
51
+ pdfGroups.forEach((pages, baseUrl) => {
52
+ const primary = pages
53
+ .slice()
54
+ .sort(
55
+ (a, b) =>
56
+ b.score - a.score || (a.pageNumber ?? 0) - (b.pageNumber ?? 0),
57
+ )[0];
58
+ const subResults = pages
59
+ .slice()
60
+ .sort((a, b) => (a.pageNumber ?? 0) - (b.pageNumber ?? 0))
61
+ .map(page => ({
62
+ title: `Page ${page.pageNumber ?? '?'}`,
63
+ url: page.url,
64
+ excerpt: page.excerpt,
65
+ }));
66
+
67
+ grouped.push({
68
+ title: primary.title,
69
+ url: baseUrl,
70
+ excerpt: primary.excerpt,
71
+ score: primary.score,
72
+ subResults,
73
+ pageNumber: null,
74
+ });
75
+ });
76
+
77
+ grouped.sort((a, b) => b.score - a.score);
78
+ return grouped;
79
+ }
@@ -8,21 +8,7 @@ header {
8
8
  gap: 0.5rem;
9
9
 
10
10
  input.search {
11
- background-color: var(--bg-color);
12
- background-image: var(--icon-search);
13
- background-repeat: no-repeat;
14
- background-position: left 6px center;
15
- background-size: 16px 16px;
16
- padding-left: 2rem;
17
-
18
- border-radius: 18px;
19
11
  flex: 1;
20
- min-width: 0;
21
-
22
- &:disabled {
23
- opacity: 0.4;
24
- cursor: not-allowed;
25
- }
26
12
  }
27
13
  }
28
14
 
@@ -75,12 +61,8 @@ header {
75
61
  color: var(--fg2-color);
76
62
 
77
63
  kbd {
78
- display: inline-block;
79
- padding: 1px 5px;
80
- border: 1px solid var(--fg2-color);
81
- border-radius: 4px;
82
- font-family: var(--font);
83
- font-size: var(--font-size-small);
64
+ border-color: var(--fg2-color);
65
+ box-shadow: 0 2px 0 var(--fg2-color);
84
66
  line-height: 1.4;
85
67
  }
86
68
  }
package/src/style.scss CHANGED
@@ -47,60 +47,18 @@
47
47
  outline-offset: 1px;
48
48
  }
49
49
 
50
- .info {
51
- @include user-select(none);
52
- display: grid;
53
- grid-template-columns: 1fr auto;
54
- align-items: baseline;
55
- color: var(--fg2-color);
56
- font-size: var(--font-size-smaller);
57
-
58
- @media (width < 450px) {
59
- grid-template-columns: 1fr;
60
- }
61
-
62
- aside.author {
63
- a {
64
- text-decoration: none;
65
-
66
- .text {
67
- color: var(--fg2-color);
68
- text-decoration: underline;
69
- text-decoration-color: var(--fg2-color);
70
- text-decoration-thickness: 1px;
71
- text-underline-offset: 3px;
72
- }
73
- }
74
-
75
- .avatar {
76
- width: 24px;
77
- height: 24px;
78
- vertical-align: middle;
79
- margin-left: 4px;
80
- }
81
- }
82
- }
83
-
84
50
  a.disabled {
85
51
  color: var(--fg-color-translucent);
86
52
  text-decoration-color: var(--fg-color-translucent);
87
53
  cursor: not-allowed;
88
- }
89
54
 
90
- @media (hover: hover) {
91
- a:hover {
92
- color: var(--fg2-color);
93
- }
94
-
95
- a.disabled:hover {
96
- color: var(--fg-color-translucent);
55
+ @media (hover: hover) {
56
+ &:hover {
57
+ color: var(--fg-color-translucent);
58
+ }
97
59
  }
98
60
  }
99
61
 
100
- a:active {
101
- color: var(--link-active-color);
102
- }
103
-
104
62
  a.external::after {
105
63
  content: '';
106
64
  background: var(--icon-external-link);
@@ -175,34 +133,6 @@ a.footnote-ref,
175
133
  @include user-select(none);
176
134
  }
177
135
 
178
- details .content > p:last-child,
179
- details .content > div:last-child {
180
- margin-bottom: 0;
181
- }
182
-
183
- details summary {
184
- cursor: pointer;
185
- @include user-select(none);
186
- padding: var(--details-padding);
187
- }
188
-
189
- details .content {
190
- padding: 0 var(--details-padding) var(--details-padding)
191
- var(--details-padding);
192
- }
193
-
194
- @media (hover: hover) {
195
- details summary {
196
- &:hover {
197
- color: var(--fg2-color);
198
- }
199
-
200
- &:active {
201
- color: var(--link-active-color);
202
- }
203
- }
204
- }
205
-
206
136
  hr {
207
137
  border: none;
208
138
  height: 32px;
@@ -223,26 +153,6 @@ hr::after {
223
153
  mask-repeat: no-repeat;
224
154
  }
225
155
 
226
- main p,
227
- main details {
228
- margin-bottom: 1rem;
229
- }
230
-
231
- main details {
232
- border-radius: var(--border-radius);
233
- border: 2px solid var(--fg-color);
234
- }
235
-
236
- input[type='text'],
237
- input[type='search'] {
238
- background: var(--bg-color);
239
- color: var(--fg-color);
240
- border: var(--control-border-width) solid var(--fg2-color);
241
- border-radius: var(--border-radius);
242
- font-size: 12pt;
243
- padding: 4px 4px;
244
- }
245
-
246
156
  button,
247
157
  a.button,
248
158
  input[type='submit'] {
@@ -279,7 +189,7 @@ input[type='submit']:focus-visible,
279
189
  input[type='text']:focus-visible,
280
190
  input[type='search']:focus-visible {
281
191
  border-color: var(--fg-color);
282
- background: var(--bg2-color);
192
+ background-color: var(--bg2-color);
283
193
  }
284
194
 
285
195
  button.icon-button {
@@ -296,10 +206,6 @@ button.icon-button {
296
206
  vertical-align: top;
297
207
  }
298
208
 
299
- li {
300
- margin-bottom: 0.5rem;
301
- }
302
-
303
209
  ol.styled-list,
304
210
  ul.styled-list {
305
211
  color: var(--theme-color-text);
@@ -470,6 +376,10 @@ dl {
470
376
  text-decoration-color: var(--fg2-color);
471
377
  }
472
378
  }
379
+
380
+ &:active {
381
+ text-decoration-color: var(--link-active-color);
382
+ }
473
383
  }
474
384
  }
475
385
 
@@ -520,14 +430,23 @@ figure.bio {
520
430
  }
521
431
 
522
432
  figcaption {
523
- font-size: var(--font-size-small);
524
- color: var(--fg2-color);
525
433
  text-align: center;
526
434
  margin-top: var(--gap);
527
435
  font-style: italic;
528
436
  }
529
437
  }
530
438
 
439
+ kbd {
440
+ display: inline-block;
441
+ padding: 1px 5px;
442
+ border: 1px solid var(--fg-color);
443
+ border-radius: 4px;
444
+ box-shadow: 0 2px 0 var(--fg-color);
445
+ font-family: var(--font);
446
+ font-size: var(--font-size-small);
447
+ font-weight: 500;
448
+ }
449
+
531
450
  select {
532
451
  font-family: var(--font);
533
452
  color: var(--fg-color);
@@ -84,14 +84,13 @@ function getOffsetMinutes(tz: string, date: Date): number {
84
84
  return (utcTs - date.getTime()) / 60000;
85
85
  }
86
86
 
87
- // window.siteVariables.timezones is replaced by DefinePlugin at build time.
88
- // The typeof guard prevents a ReferenceError in test environments where
89
- // window is not defined.
90
- const TIMEZONES: TimezoneDef[] =
91
- typeof window !== 'undefined' ? window.siteVariables.timezones : [];
87
+ // typeof guard prevents ReferenceError when tests import this module
88
+ // without going through the bundler (which substitutes __SITE_TIMEZONES__).
89
+ const TIMEZONES: TimeZone[] =
90
+ typeof __SITE_TIMEZONES__ !== 'undefined' ? __SITE_TIMEZONES__ : [];
92
91
 
93
92
  function getDefaultTimezone() {
94
- const value = window.siteVariables.defaultTimeZone;
93
+ const value = __SITE_DEFAULT_TIMEZONE__;
95
94
  return TIMEZONES.find(tz => tz.value === value)!;
96
95
  }
97
96
 
@@ -101,25 +100,10 @@ function computeOffsets(baseDate: Date) {
101
100
  });
102
101
  }
103
102
 
104
- function init(element: HTMLDataListElement, selectedTz: string) {
105
- const beforeText = window.document.createTextNode('Times shown in ');
106
- element.parentElement?.insertBefore(beforeText, element);
107
-
108
- TIMEZONES.forEach(tz => {
109
- const opt = document.createElement('option');
110
- opt.value = tz.value;
111
- if (tz.value === 'UTC') {
112
- opt.textContent = tz.label;
113
- } else {
114
- opt.textContent = `${tz.label} (${tz.abbreviation})`;
115
- }
116
- if (tz.value === selectedTz) {
117
- opt.selected = true;
118
- }
119
- element.appendChild(opt);
120
- });
121
-
122
- element.removeAttribute('hidden');
103
+ function init(element: HTMLSelectElement, selectedTz: string) {
104
+ element.value = selectedTz;
105
+ element.hidden = false;
106
+ element.disabled = false;
123
107
  }
124
108
 
125
109
  function dominantPeriodStyle(
package/src/util.ts CHANGED
@@ -34,7 +34,7 @@ export function applyBasePath(subPath: string): string {
34
34
  throw new Error('invalid internal path, must start with "/": ' + subPath);
35
35
  }
36
36
 
37
- let path = window.siteVariables.basePath || '/';
37
+ let path = __SITE_BASE_PATH__;
38
38
  if (path.endsWith('/')) {
39
39
  path = path.slice(0, -1);
40
40
  }
@@ -42,6 +42,7 @@
42
42
  }]
43
43
  }
44
44
  </script>
45
+ <script>document.head.appendChild(document.createElement('style')).textContent='.time-zone-label[hidden],select.time-zone[hidden]{display:revert}'</script>
45
46
  <noscript><style>
46
47
  .question-a-body:not([data-revealed]) > * { color: unset; background-color: unset; }
47
48
  .question-a-body:not([data-revealed]) > * * { color: unset; background-color: unset; }
@@ -54,7 +55,7 @@
54
55
  <header data-pagefind-ignore>
55
56
  <details>
56
57
  <summary aria-label="Toggle site navigation">
57
- <span class="logo" aria-hidden="true"><%= site.symbol %></span>
58
+ <% if (site.symbol) { %><span class="logo" aria-hidden="true"><%= site.symbol %></span><% } %>
58
59
  <span class="site-title" aria-hidden="true"><%= site.title %></span>
59
60
  <div id="to-top-container" class="to-top-container"></div>
60
61
  <% if (site.features.search) { %>
package/types/dev.d.ts CHANGED
@@ -1,3 +1 @@
1
- interface Window {
2
- IS_DEV: boolean | undefined;
3
- }
1
+ declare const __IS_DEV__: boolean;