@abreen/tada 1.1.0 → 1.2.0

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 (88) hide show
  1. package/README.md +68 -16
  2. package/bin/tada.ts +289 -24
  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 +9 -11
  21. package/build/globals.ts +6 -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 +118 -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 +3 -3
  32. package/build/site-variables.ts +13 -5
  33. package/build/templates.ts +7 -6
  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/literate-java.ts +11 -10
  46. package/build/utils/markdown.ts +41 -9
  47. package/build/utils/paths.test.ts +2 -2
  48. package/build/utils/paths.ts +5 -1
  49. package/build/utils/render.ts +45 -32
  50. package/build/utils/shiki-highlighter.ts +1 -1
  51. package/build/validate-internal-links-plugin.test.ts +1 -1
  52. package/build/validate-internal-links-plugin.ts +1 -1
  53. package/build/watch-reachability-state.test.ts +2 -2
  54. package/build/watch-reachability-state.ts +1 -1
  55. package/build/watch.ts +74 -98
  56. package/{content → init/content}/lectures/index.md +1 -1
  57. package/{content → init/content}/markdown.md +26 -7
  58. package/package.json +9 -7
  59. package/{templates → schema}/nav.schema.json +1 -2
  60. package/{build → schema}/site.schema.json +1 -2
  61. package/src/_alerts.scss +4 -0
  62. package/src/_base.scss +41 -2
  63. package/src/_layout.scss +37 -0
  64. package/src/anchor/index.ts +0 -1
  65. package/src/index.ts +3 -3
  66. package/src/search/index.test.ts +203 -0
  67. package/src/search/index.ts +3 -79
  68. package/src/search/pdf-utils.ts +79 -0
  69. package/src/search/style.scss +0 -14
  70. package/src/style.scss +9 -57
  71. package/src/timezone/index.ts +5 -6
  72. package/src/util.ts +1 -1
  73. package/templates/_top.html +1 -1
  74. package/types/dev.d.ts +1 -3
  75. package/types/site-variables.d.ts +6 -10
  76. package/types/untyped-modules.d.ts +7 -0
  77. package/config/authors.json +0 -1
  78. package/config/nav.json +0 -28
  79. /package/{content → init/content}/index.md +0 -0
  80. /package/{content → init/content}/lectures/01/Pair.java.md +0 -0
  81. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  82. /package/{content → init/content}/lectures/01/demo.py +0 -0
  83. /package/{content → init/content}/lectures/01/index.md +0 -0
  84. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  85. /package/{content → init/content}/problem_sets/index.html +0 -0
  86. /package/{public → init/public}/avatars/alex.jpg +0 -0
  87. /package/{public → init/public}/test.txt +0 -0
  88. /package/{templates → schema}/authors.schema.json +0 -0
package/build/watch.ts CHANGED
@@ -4,37 +4,33 @@ import { fork } from 'child_process';
4
4
  import path from 'path';
5
5
  import chokidar from 'chokidar';
6
6
  import WebSocket, { WebSocketServer } from 'ws';
7
- import type { SiteVariables, WatchState } from './types.js';
8
- import { B } from './colors.js';
9
- import { makeLogger, printFlair } from './log.js';
10
- import { getDevSiteVariables } from './site-variables.js';
11
- import {
12
- compileTemplates,
13
- getHtmlTemplatesDir,
14
- getJsonDataDir,
15
- JSON_DATA_FILES,
16
- } from './templates.js';
7
+ import type { SiteVariables, WatchState } from './types';
8
+ import { B } from './colors';
9
+ import { makeLogger, printFlair } from './log';
10
+ import { getDevSiteVariables } from './site-variables';
11
+ import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
17
12
  import {
18
13
  getContentDir,
19
14
  getPublicDir,
20
15
  getDistDir,
21
16
  getPackageDir,
22
- } from './utils/paths.js';
23
- import { isFeatureEnabled } from './features.js';
24
- import { bundle } from './bundle.js';
25
- import { copyFonts } from './generate-fonts.js';
26
- import { generateFavicons } from './generate-favicon.js';
27
- import { generateManifest } from './generate-manifest.js';
17
+ } from './utils/paths';
18
+ import { isFeatureEnabled } from './features';
19
+ import { bundle } from './bundle';
20
+ import { copyFonts } from './generate-fonts';
21
+ import { copyKatexAssets } from './generate-katex-assets';
22
+ import { generateFavicons } from './generate-favicon';
23
+ import { generateManifest } from './generate-manifest';
28
24
  import {
29
25
  copyPublicFiles,
30
26
  copyContentAssets,
31
27
  copyContentFile,
32
28
  copyPublicFile,
33
- } from './copy.js';
34
- import { getProcessedExtensions } from './utils/file-types.js';
35
- import { ContentRenderer } from './generate-content-assets.js';
36
- import { WatchPagefindRunner } from './pagefind.js';
37
- import { ContentChangeDetector } from './content-watch.js';
29
+ } from './copy';
30
+ import { getProcessedExtensions } from './utils/file-types';
31
+ import { ContentRenderer } from './generate-content-assets';
32
+ import { WatchPagefindRunner } from './pagefind';
33
+ import { ContentChangeDetector } from './content-watch';
38
34
 
39
35
  function getArg(name: string): number | null {
40
36
  const idx = process.argv.indexOf(name);
@@ -74,7 +70,7 @@ async function bundleReloadClient(): Promise<string[]> {
74
70
  );
75
71
  }
76
72
 
77
- type ChangeCategory = 'content' | 'public' | 'src' | 'templates' | 'config';
73
+ type ChangeCategory = 'content' | 'public' | 'config';
78
74
 
79
75
  const log = makeLogger(__filename);
80
76
  const wslog = makeLogger('WebSocket');
@@ -82,6 +78,7 @@ const wslog = makeLogger('WebSocket');
82
78
  // --- WebSocket server (unchanged) ---
83
79
 
84
80
  let webSocketsReady = false;
81
+ let watcherReady = false;
85
82
  let webServerReady = false;
86
83
  let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
87
84
  let serveStarted = false;
@@ -92,6 +89,12 @@ try {
92
89
 
93
90
  wss.on('connection', (conn: WebSocket) => {
94
91
  wslog.debug`WebSocket client connected`;
92
+ if (watcherReady && conn.readyState === WebSocket.OPEN) {
93
+ if (initialBuildFailed) {
94
+ conn.send('error');
95
+ }
96
+ conn.send('ready');
97
+ }
95
98
  conn.on('close', () => {
96
99
  wslog.debug`WebSocket client disconnected`;
97
100
  });
@@ -196,12 +199,9 @@ function toPublicRelativePath(filePath: string): string | null {
196
199
  const contentDir: string = getContentDir();
197
200
  const publicDir: string = getPublicDir();
198
201
  const distDir: string = getDistDir();
199
- const packageDir: string = getPackageDir();
200
202
 
201
- let siteVariables: SiteVariables = getDevSiteVariables();
202
- let processedExtSet = new Set<string>(
203
- getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
204
- );
203
+ let siteVariables: SiteVariables;
204
+ let processedExtSet = new Set<string>();
205
205
  let publicRelPaths: Set<string> = new Set();
206
206
  let contentAssetRelPaths: Set<string> = new Set();
207
207
  let contentRenderer: ContentRenderer;
@@ -213,6 +213,10 @@ const pendingChanges = new Set<string>();
213
213
  let rebuilding = false;
214
214
 
215
215
  async function initialBuild(): Promise<void> {
216
+ siteVariables = getDevSiteVariables();
217
+ processedExtSet = new Set(
218
+ getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
219
+ );
216
220
  fs.mkdirSync(distDir, { recursive: true });
217
221
 
218
222
  compileTemplates(siteVariables);
@@ -231,6 +235,7 @@ async function initialBuild(): Promise<void> {
231
235
  ];
232
236
 
233
237
  copyFonts(distDir);
238
+ copyKatexAssets(distDir);
234
239
 
235
240
  if (isFeatureEnabled(siteVariables, 'favicon')) {
236
241
  await generateFavicons(siteVariables, distDir);
@@ -270,8 +275,6 @@ function classifyChange(filePath: string): ChangeCategory | null {
270
275
  const resolved = path.resolve(filePath);
271
276
  const resolvedContentDir = path.resolve(contentDir) + path.sep;
272
277
  const resolvedPublicDir = path.resolve(publicDir) + path.sep;
273
- const resolvedSrcDir = path.resolve(packageDir, 'src') + path.sep;
274
- const htmlTemplatesDir = path.resolve(getHtmlTemplatesDir()) + path.sep;
275
278
  const jsonDataDir = getJsonDataDir();
276
279
  const siteConfigPath = path.resolve('site.dev.json');
277
280
 
@@ -281,16 +284,10 @@ function classifyChange(filePath: string): ChangeCategory | null {
281
284
  if (resolved.startsWith(resolvedPublicDir)) {
282
285
  return 'public';
283
286
  }
284
- if (resolved.startsWith(resolvedSrcDir)) {
285
- return 'src';
286
- }
287
287
  if (
288
- resolved.startsWith(htmlTemplatesDir) ||
288
+ resolved === siteConfigPath ||
289
289
  JSON_DATA_FILES.some(f => resolved === path.resolve(jsonDataDir, f))
290
290
  ) {
291
- return 'templates';
292
- }
293
- if (resolved === siteConfigPath) {
294
291
  return 'config';
295
292
  }
296
293
  return null;
@@ -316,6 +313,8 @@ async function rebuild(): Promise<void> {
316
313
  }
317
314
  }
318
315
 
316
+ let succeeded = false;
317
+
319
318
  try {
320
319
  // Site config changed, full restart
321
320
  if (categories.has('config')) {
@@ -337,61 +336,36 @@ async function rebuild(): Promise<void> {
337
336
  ...(await bundleReloadClient()),
338
337
  ];
339
338
 
340
- const result = contentRenderer.processContent({ distDir, assetFiles });
341
- for (const err of result.errors) {
342
- log.error`${err.message}`;
343
- }
344
- if (result.errors.length === 0) {
345
- printFlair();
346
- broadcast('reload');
347
- if (pagefindRunner) {
348
- pagefindRunner.update(distDir, result.htmlAssetsByPath);
349
- setImmediate(() => pagefindRunner!.run());
350
- }
339
+ copyFonts(distDir);
340
+ copyKatexAssets(distDir);
341
+
342
+ if (isFeatureEnabled(siteVariables, 'favicon')) {
343
+ await generateFavicons(siteVariables, distDir);
344
+ generateManifest(siteVariables, distDir);
351
345
  }
352
- rebuilding = false;
353
- return;
354
- }
355
346
 
356
- // Source changed, re-bundle, then re-render all content
357
- if (categories.has('src')) {
358
- assetFiles = [
359
- ...(await bundle(siteVariables, { mode: 'development' })),
360
- ...(await bundleReloadClient()),
361
- ];
362
- // Force full content re-render since asset filenames may have changed
363
347
  const result = contentRenderer.processContent({ distDir, assetFiles });
364
348
  for (const err of result.errors) {
365
349
  log.error`${err.message}`;
366
350
  }
367
351
  if (result.errors.length === 0) {
368
352
  printFlair();
353
+ if (!serveStarted) {
354
+ serveStarted = true;
355
+ serve();
356
+ }
369
357
  broadcast('reload');
370
358
  if (pagefindRunner) {
371
359
  pagefindRunner.update(distDir, result.htmlAssetsByPath);
372
360
  setImmediate(() => pagefindRunner!.run());
373
361
  }
362
+ succeeded = true;
374
363
  }
375
- rebuilding = false;
376
364
  return;
377
365
  }
378
366
 
379
- // Templates/data changed, recompile templates, re-render all content
380
- if (categories.has('templates')) {
381
- const detection = changeDetector.detectChanges(changes);
382
- if (detection.templateError) {
383
- log.error`Template error: ${detection.templateError.message}`;
384
- rebuilding = false;
385
- return;
386
- }
387
- }
388
-
389
367
  // Public file changed, copy just that file
390
- if (
391
- categories.has('public') &&
392
- !categories.has('content') &&
393
- !categories.has('templates')
394
- ) {
368
+ if (categories.has('public') && !categories.has('content')) {
395
369
  for (const filePath of changes) {
396
370
  if (classifyChange(filePath) === 'public') {
397
371
  const absPath = path.resolve(filePath);
@@ -407,17 +381,12 @@ async function rebuild(): Promise<void> {
407
381
  }
408
382
  printFlair();
409
383
  broadcast('reload');
410
- rebuilding = false;
384
+ succeeded = true;
411
385
  return;
412
386
  }
413
387
 
414
- // Content and/or templates changed, incremental rebuild
388
+ // Content changed, incremental rebuild
415
389
  const detection = changeDetector.detectChanges(changes);
416
- if (detection.templateError) {
417
- log.error`Template error: ${detection.templateError.message}`;
418
- rebuilding = false;
419
- return;
420
- }
421
390
 
422
391
  if (detection.needsRestart) {
423
392
  log.event`Content structure changed, full rebuild`;
@@ -493,16 +462,24 @@ async function rebuild(): Promise<void> {
493
462
 
494
463
  if (result.errors.length === 0) {
495
464
  printFlair();
465
+ if (!serveStarted) {
466
+ serveStarted = true;
467
+ serve();
468
+ }
496
469
  broadcast('reload');
497
470
 
498
471
  if (pagefindRunner) {
499
472
  pagefindRunner.update(distDir, result.htmlAssetsByPath);
500
473
  setImmediate(() => pagefindRunner!.run());
501
474
  }
475
+ succeeded = true;
502
476
  }
503
477
  } catch (err) {
504
478
  log.error`Build failed: ${(err as Error).message}`;
505
479
  } finally {
480
+ if (!succeeded) {
481
+ broadcast('error');
482
+ }
506
483
  rebuilding = false;
507
484
 
508
485
  // If more changes accumulated during rebuild, schedule another
@@ -526,22 +503,18 @@ function onFileChange(filePath: string): void {
526
503
 
527
504
  // --- Start ---
528
505
 
506
+ let initialBuildFailed = false;
507
+
529
508
  initialBuild()
530
- .then(() => {
531
- // Watch content, public, src, templates, data files, site config
509
+ .catch(err => {
510
+ log.error`Initial build failed: ${(err as Error).message}`;
511
+ initialBuildFailed = true;
512
+ })
513
+ .finally(() => {
514
+ // Watch content, public, data files, site config
532
515
  const watchPaths: string[] = [contentDir, publicDir];
533
516
 
534
- // Watch package src/ and templates/ for Tada development
535
- const srcDir = path.resolve(packageDir, 'src');
536
- const templatesDir = getHtmlTemplatesDir();
537
- if (fs.existsSync(srcDir)) {
538
- watchPaths.push(srcDir);
539
- }
540
- if (fs.existsSync(templatesDir)) {
541
- watchPaths.push(templatesDir);
542
- }
543
-
544
- // Watch data files
517
+ // Watch data files (nav.json, authors.json)
545
518
  const jsonDataDir = getJsonDataDir();
546
519
  for (const dataFile of JSON_DATA_FILES) {
547
520
  const dataPath = path.join(jsonDataDir, dataFile);
@@ -565,9 +538,12 @@ initialBuild()
565
538
  watcher.on('add', onFileChange);
566
539
  watcher.on('unlink', onFileChange);
567
540
 
568
- log.info`Watching for changes...`;
569
- })
570
- .catch(err => {
571
- log.error`Initial build failed: ${(err as Error).message}`;
572
- process.exit(1);
541
+ watcher.on('ready', () => {
542
+ watcherReady = true;
543
+ if (initialBuildFailed) {
544
+ broadcast('error');
545
+ }
546
+ broadcast('ready');
547
+ log.info`Watching for changes...`;
548
+ });
573
549
  });
@@ -2,7 +2,7 @@ title: Lectures
2
2
  author: alex
3
3
  description: An example page for <%= site.title %>.
4
4
 
5
- <%= timezoneChooser %>
5
+ <%= renderTimeZoneChooser() %>
6
6
 
7
7
  One-hour lectures are held weekly at <time datetime="17:40">5:40 PM</time>.
8
8
  Each lecture is recorded and made available by password-protected link.
@@ -128,6 +128,20 @@ See `config/site.dev.json` and `config/site.prod.json` to change `site.basePath`
128
128
  and `site.internalDomains`.
129
129
 
130
130
 
131
+ ## LaTeX
132
+
133
+ $\LaTeX$ support is implemented via [KaTeX](https://katex.org/). The standard
134
+ dollar sign-based syntax is supported. Wrap inline math in single `$`,
135
+ or use `$$` for a math block.
136
+
137
+ Here's an example: selection sort performs $O(n^2)$ comparisons. And to
138
+ demonstrate a block, here's the exact number of comparisons:
139
+
140
+ $$
141
+ (n-1) + (n-2) + \dots + 1 = \sum_{i=1}^{n-1}i
142
+ $$
143
+
144
+
131
145
  ## Additional syntax
132
146
 
133
147
  !!! note Headings are links
@@ -228,7 +242,7 @@ Beware of this rule.
228
242
  A time zone chooser for testing styles (see the "time zone chooser" section
229
243
  below):
230
244
 
231
- <%= timezoneChooser %>
245
+ <%= renderTimeZoneChooser() %>
232
246
 
233
247
  A `<time>` element for testing styles: <time datetime="15:00">3 pm</time>
234
248
 
@@ -326,14 +340,19 @@ These features aren't Markdown-specific, but are included here for reference.
326
340
 
327
341
  Allows the user to see times in their local time zone.
328
342
 
329
- !!! note
330
- Look at the source code for this file to see how to insert it.
331
- !!!
343
+ Insert the time zone chooser using Lodash templates, calling the
344
+ `renderTimeZoneChooser()` function (made available on any page as a global):
345
+
346
+ ```
347
+ <%= '\x3C%= renderTimeZoneChooser() %\x3E' %>
348
+ ```
349
+
350
+ To produce:
332
351
 
333
- <%= timezoneChooser %>
352
+ <%= renderTimeZoneChooser() %>
334
353
 
335
- Wrap your times in `<time datetime="...">` elements and they will be updated
336
- when the user makes a time zone selection.
354
+ Then, wrap your times in `<time datetime="...">` elements and they will be
355
+ updated when the user makes a time zone selection.
337
356
 
338
357
  Use
339
358
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -13,24 +13,25 @@
13
13
  "src/",
14
14
  "templates/",
15
15
  "fonts/",
16
- "config/",
17
- "content/",
18
- "public/",
16
+ "init/",
17
+ "schema/",
19
18
  "types/",
20
19
  "tsconfig.json"
21
20
  ],
22
21
  "scripts": {
23
- "prepare": "husky",
24
- "init-example": "bun run bin/tada.ts init example --default",
22
+ "prepare": "husky && bun run scripts/check-deps.ts",
23
+ "init-example": "bun run bin/tada.ts init example --no-interactive",
25
24
  "lint": "eslint src/ build/ bin/",
26
25
  "typecheck": "tsc --noEmit",
27
- "test:functional": "python3 -m pip install -q -r functional_tests/requirements.txt && python3 -m pytest functional_tests/ -v -n auto"
26
+ "test:functional": "python3 -m pip install -q -r functional_tests/requirements.txt && python3 -m pytest functional_tests/ -v -n auto --durations=10",
27
+ "test:all": "bun test && bun run test:functional"
28
28
  },
29
29
  "engines": {
30
30
  "bun": ">=1.0.0"
31
31
  },
32
32
  "dependencies": {
33
33
  "@tsconfig/node23": "^23.0.2",
34
+ "@vscode/markdown-it-katex": "^1.1.2",
34
35
  "ajv": "^8.17.1",
35
36
  "chokidar": "^4.0.0",
36
37
  "culori": "^4.0.2",
@@ -38,6 +39,7 @@
38
39
  "front-matter": "^4.0.2",
39
40
  "java-parser": "^3.0.1",
40
41
  "jsdom": "^26.1.0",
42
+ "katex": "^0.16.40",
41
43
  "lodash": "^4.17.21",
42
44
  "markdown-it": "^14.1.0",
43
45
  "markdown-it-anchor": "^9.2.0",
@@ -14,8 +14,7 @@
14
14
  "text": { "type": "string" },
15
15
  "internal": { "type": "string", "pattern": "^\\/.*$" },
16
16
  "external": { "type": "string", "pattern": "^https?:\\/\\/.*$" },
17
- "disabled": { "type": "boolean" },
18
- "keywords": { "type": "array", "items": { "type": "string" } }
17
+ "disabled": { "type": "boolean" }
19
18
  },
20
19
  "oneOf": [{ "required": ["internal"] }, { "required": ["external"] }],
21
20
  "additionalProperties": false
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["defaultTimeZone"],
3
+ "required": ["base", "title", "defaultTimeZone", "themeColor"],
4
4
  "properties": {
5
5
  "features": {
6
6
  "type": "object",
@@ -27,7 +27,6 @@
27
27
  "title": { "type": "string" },
28
28
  "symbol": { "type": "string", "pattern": "^[A-Z0-9\\- ]{1,5}$" },
29
29
  "titlePostfix": { "type": "string" },
30
- "courseId": { "type": "string" },
31
30
  "themeColor": { "type": "string" },
32
31
  "tintHue": {
33
32
  "type": "integer",
package/src/_alerts.scss CHANGED
@@ -40,6 +40,10 @@
40
40
 
41
41
  a {
42
42
  text-decoration-color: var(--fg-color);
43
+
44
+ &:hover {
45
+ text-decoration-color: inherit;
46
+ }
43
47
  }
44
48
 
45
49
  dfn {
package/src/_base.scss CHANGED
@@ -74,8 +74,21 @@ html {
74
74
  a {
75
75
  color: var(--fg-color);
76
76
  text-decoration-color: var(--fg2-color);
77
- text-decoration-thickness: 1px;
78
- text-underline-offset: 3px;
77
+ text-decoration-thickness: 1.5px;
78
+
79
+ @media (hover: hover) {
80
+ &:hover {
81
+ color: var(--fg2-color);
82
+ }
83
+ }
84
+ }
85
+
86
+ a:active {
87
+ color: var(--link-active-color);
88
+
89
+ @media (hover: hover) {
90
+ text-decoration-color: var(--link-active-color);
91
+ }
79
92
  }
80
93
 
81
94
  pre,
@@ -104,3 +117,29 @@ pre {
104
117
  padding: 0;
105
118
  }
106
119
  }
120
+
121
+ input[type='text'],
122
+ input[type='search'] {
123
+ background: var(--bg-color);
124
+ color: var(--fg-color);
125
+ border: var(--control-border-width) solid var(--fg2-color);
126
+ border-radius: var(--border-radius);
127
+ font-size: 12pt;
128
+ padding: 4px 4px;
129
+ }
130
+
131
+ input[type='search'] {
132
+ background-color: var(--bg-color);
133
+ background-image: var(--icon-search);
134
+ background-repeat: no-repeat;
135
+ background-position: left 6px center;
136
+ background-size: 16px 16px;
137
+ padding-left: 2rem;
138
+ border-radius: 18px;
139
+ min-width: 0;
140
+
141
+ &:disabled {
142
+ opacity: 0.4;
143
+ cursor: not-allowed;
144
+ }
145
+ }
package/src/_layout.scss CHANGED
@@ -1,3 +1,5 @@
1
+ @use 'mixins' as *;
2
+
1
3
  :root {
2
4
  --toc-width: 260px;
3
5
  }
@@ -8,6 +10,7 @@ html {
8
10
  overflow-y: auto;
9
11
  scroll-padding-top: calc(var(--header-collapsed-height) + var(--gap));
10
12
  }
13
+
11
14
  body {
12
15
  margin: 0;
13
16
  padding: 0;
@@ -132,6 +135,40 @@ body.literate main table {
132
135
  }
133
136
  }
134
137
 
138
+ .info {
139
+ @include user-select(none);
140
+ display: grid;
141
+ grid-template-columns: 1fr auto;
142
+ align-items: baseline;
143
+ color: var(--fg2-color);
144
+ font-size: var(--font-size-smaller);
145
+
146
+ @media (width < 450px) {
147
+ grid-template-columns: 1fr;
148
+ }
149
+
150
+ aside.author {
151
+ a {
152
+ text-decoration: none;
153
+
154
+ .text {
155
+ color: var(--fg2-color);
156
+ text-decoration: underline;
157
+ text-decoration-color: var(--fg2-color);
158
+ text-decoration-thickness: 1px;
159
+ text-underline-offset: 3px;
160
+ }
161
+ }
162
+
163
+ .avatar {
164
+ width: 24px;
165
+ height: 24px;
166
+ vertical-align: middle;
167
+ margin-left: 4px;
168
+ }
169
+ }
170
+ }
171
+
135
172
  .columns {
136
173
  display: grid;
137
174
  grid-template-columns: max-content auto;
@@ -17,7 +17,6 @@ export default (window: Window) => {
17
17
  link.className = 'heading-anchor';
18
18
  link.href = `#${el.id}`;
19
19
  link.title = 'Link to this heading';
20
- link.setAttribute('aria-label', 'Link to this heading');
21
20
 
22
21
  // Move all existing child nodes into the link
23
22
  while (el.firstChild) {
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import mountPrint from './print';
18
18
  import mountTop from './top';
19
19
  import mountAnchor from './anchor';
20
20
  import mountQuestion from './question';
21
- import mountTimezone from './timezone';
21
+ import mountTimeZone from './timezone';
22
22
  import mountCode from './code';
23
23
 
24
24
  import { scheduleTask, formatDuration } from './util';
@@ -31,7 +31,7 @@ const COMPONENTS = {
31
31
  top: mountTop,
32
32
  anchor: mountAnchor,
33
33
  question: mountQuestion,
34
- timezone: mountTimezone,
34
+ timeZone: mountTimeZone,
35
35
  code: mountCode,
36
36
  };
37
37
 
@@ -65,7 +65,7 @@ document.addEventListener('DOMContentLoaded', async () => {
65
65
  console.error(`Failed to mount ${name} component:`, reason);
66
66
  }
67
67
 
68
- if (window.IS_DEV) {
68
+ if (__IS_DEV__) {
69
69
  const diff = window.performance.now() - startTime;
70
70
  console.info(`Components mounted in ${formatDuration(diff)}`);
71
71
  }