@abreen/tada 1.13.4 → 1.13.7

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/build/bundle.ts CHANGED
@@ -9,10 +9,20 @@ import {
9
9
  getDistDir,
10
10
  toPosix,
11
11
  } from './utils/paths';
12
- import { deriveTheme, deriveLinkHue } from './utils/derive-theme';
12
+ import {
13
+ deriveTheme,
14
+ deriveLinkHue,
15
+ deriveTraceLineActiveHue,
16
+ } from './utils/derive-theme';
13
17
  import type { PluginBuilder } from 'bun';
14
18
  import type { SiteVariables } from './types';
15
19
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
20
+ import pkg from '../package.json' with { type: 'json' };
21
+
22
+ export function getBundleNaming(): string {
23
+ const version = pkg.version.replace(/[^a-zA-Z0-9.-]/g, '-');
24
+ return `[name].bundle.tada-${version}.[ext]`;
25
+ }
16
26
 
17
27
  function formatCssNumber(value: number, precision = 4): string {
18
28
  if (Number.isInteger(value)) {
@@ -40,6 +50,9 @@ function renderThemeScss(siteVariables: SiteVariables): string {
40
50
  const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
41
51
  const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
42
52
  const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
53
+ const traceLineActiveHue = formatCssNumber(deriveTraceLineActiveHue(tintHue));
54
+ const bgTraceLineActive = `hsl(${traceLineActiveHue}deg 100% 86%)`;
55
+ const bgTraceLineActiveDark = `hsl(${traceLineActiveHue}deg 90% 18%)`;
43
56
 
44
57
  const rendered = _.template(template)({
45
58
  ...theme,
@@ -54,6 +67,8 @@ function renderThemeScss(siteVariables: SiteVariables): string {
54
67
  linkColorHover,
55
68
  linkColorDark,
56
69
  linkColorHoverDark,
70
+ bgTraceLineActive,
71
+ bgTraceLineActiveDark,
57
72
  });
58
73
 
59
74
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
@@ -109,7 +124,7 @@ export async function bundle(
109
124
  const result = await Bun.build({
110
125
  entrypoints,
111
126
  outdir: resolvedDistDir,
112
- naming: '[name].bundle.[ext]',
127
+ naming: getBundleNaming(),
113
128
  minify: mode === 'production',
114
129
  sourcemap: isDev ? 'inline' : 'none',
115
130
  define: createDefine(siteVariables, isDev),
@@ -35,6 +35,7 @@ export class ContentRenderer {
35
35
  string,
36
36
  {
37
37
  manifestUrl: string;
38
+ artifactId: string;
38
39
  highlightedSource: string;
39
40
  totalSteps: number;
40
41
  mtime: number;
package/build/types.d.ts CHANGED
@@ -97,6 +97,7 @@ export interface RenderPlainTextOptions {
97
97
  string,
98
98
  {
99
99
  manifestUrl: string;
100
+ artifactId: string;
100
101
  highlightedSource: string;
101
102
  totalSteps: number;
102
103
  mtime: number;
@@ -16,6 +16,8 @@ const DARK_TEXT_L_MAX = 0.82;
16
16
 
17
17
  const LINK_ANCHOR_HUE = 212;
18
18
  const LINK_TINT_BLEND = 0.05;
19
+ const TRACE_LINE_ACTIVE_ANCHOR_HUE = 52;
20
+ const TRACE_LINE_ACTIVE_TINT_BLEND = 0.1;
19
21
 
20
22
  function clamp(v: number, min: number, max: number): number {
21
23
  return Math.min(Math.max(v, min), max);
@@ -83,6 +85,14 @@ export function deriveLinkHue(tintHue: number): number {
83
85
  return (LINK_ANCHOR_HUE + diff * LINK_TINT_BLEND + 360) % 360;
84
86
  }
85
87
 
88
+ export function deriveTraceLineActiveHue(tintHue: number): number {
89
+ const diff = ((tintHue - TRACE_LINE_ACTIVE_ANCHOR_HUE + 540) % 360) - 180;
90
+ return (
91
+ (TRACE_LINE_ACTIVE_ANCHOR_HUE + diff * TRACE_LINE_ACTIVE_TINT_BLEND + 360) %
92
+ 360
93
+ );
94
+ }
95
+
86
96
  export function getTextOnColor(cssColor: string): '#fff' | '#000' {
87
97
  const parsed = parse(cssColor);
88
98
  if (!parsed) {
@@ -432,6 +432,7 @@ function renderPlainTextContent(
432
432
  string,
433
433
  {
434
434
  manifestUrl: string;
435
+ artifactId: string;
435
436
  highlightedSource: string;
436
437
  totalSteps: number;
437
438
  mtime: number;
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { createHash } from 'crypto';
3
4
  import { renderCodeSegment } from './code';
4
5
  import { splitLines } from './literate-java';
5
6
  import { normalizeOutputPath } from './paths';
@@ -14,18 +15,46 @@ export interface ChunkTraceOutputOptions {
14
15
  ignoreFields?: Record<string, string[]>;
15
16
  }
16
17
 
18
+ export interface ChunkTraceOutputResult {
19
+ manifest: TraceManifest;
20
+ artifactId: string;
21
+ outputPaths: string[];
22
+ }
23
+
24
+ function hashTraceFiles(files: { name: string; content: string }[]): string {
25
+ const hasher = createHash('sha256');
26
+ for (const file of files) {
27
+ hasher.update(file.name, 'utf8');
28
+ hasher.update('\0', 'utf8');
29
+ hasher.update(file.content, 'utf8');
30
+ hasher.update('\0', 'utf8');
31
+ }
32
+ return `sha256-${hasher.digest('hex').slice(0, 16)}`;
33
+ }
34
+
35
+ function buildTraceOutputPath(
36
+ relDir: string,
37
+ traceName: string,
38
+ artifactId: string,
39
+ fileName: string,
40
+ ): string {
41
+ return [relDir, '_traces', traceName, artifactId, fileName]
42
+ .filter(Boolean)
43
+ .join('/');
44
+ }
45
+
17
46
  export function chunkTraceOutput(
18
47
  output: string,
19
48
  traceOutputDir: string,
49
+ relDir: string,
50
+ traceName: string,
20
51
  sourceFile: string,
21
52
  source: string,
22
53
  options: ChunkTraceOutputOptions = {},
23
- ): TraceManifest {
54
+ ): ChunkTraceOutputResult {
24
55
  const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
25
56
  const ignoreFields = options.ignoreFields ?? {};
26
57
 
27
- fs.mkdirSync(traceOutputDir, { recursive: true });
28
-
29
58
  const lines = output
30
59
  .trim()
31
60
  .split('\n')
@@ -46,26 +75,27 @@ export function chunkTraceOutput(
46
75
 
47
76
  let chunkEntries: TraceChunkEntry[] = [];
48
77
  let chunkIndex = 0;
78
+ const chunkFiles: { name: string; content: string }[] = [];
49
79
 
50
80
  for (const step of allSteps) {
51
81
  const svg = generateStepSvg(step, layout);
52
82
  chunkEntries.push({ line: step.line, stdout: step.stdout, svg });
53
83
 
54
84
  if (chunkEntries.length >= chunkSize) {
55
- fs.writeFileSync(
56
- path.join(traceOutputDir, `chunk-${chunkIndex}.json`),
57
- JSON.stringify(chunkEntries),
58
- );
85
+ chunkFiles.push({
86
+ name: `chunk-${chunkIndex}.json`,
87
+ content: JSON.stringify(chunkEntries),
88
+ });
59
89
  chunkEntries = [];
60
90
  chunkIndex++;
61
91
  }
62
92
  }
63
93
 
64
94
  if (chunkEntries.length > 0) {
65
- fs.writeFileSync(
66
- path.join(traceOutputDir, `chunk-${chunkIndex}.json`),
67
- JSON.stringify(chunkEntries),
68
- );
95
+ chunkFiles.push({
96
+ name: `chunk-${chunkIndex}.json`,
97
+ content: JSON.stringify(chunkEntries),
98
+ });
69
99
  }
70
100
 
71
101
  const manifest: TraceManifest = {
@@ -76,12 +106,26 @@ export function chunkTraceOutput(
76
106
  lineToSteps,
77
107
  };
78
108
 
79
- fs.writeFileSync(
80
- path.join(traceOutputDir, 'manifest.json'),
81
- JSON.stringify(manifest),
82
- );
109
+ const manifestFile = {
110
+ name: 'manifest.json',
111
+ content: JSON.stringify(manifest),
112
+ };
113
+ const files = [manifestFile, ...chunkFiles];
114
+ const artifactId = hashTraceFiles(files);
115
+ const artifactDir = path.join(traceOutputDir, artifactId);
83
116
 
84
- return manifest;
117
+ fs.mkdirSync(artifactDir, { recursive: true });
118
+ for (const file of files) {
119
+ fs.writeFileSync(path.join(artifactDir, file.name), file.content);
120
+ }
121
+
122
+ return {
123
+ manifest,
124
+ artifactId,
125
+ outputPaths: files.map(file =>
126
+ buildTraceOutputPath(relDir, traceName, artifactId, file.name),
127
+ ),
128
+ };
85
129
  }
86
130
 
87
131
  export function highlightTraceSource(
@@ -96,16 +140,25 @@ export function highlightTraceSource(
96
140
  export function getTraceOutputPaths(
97
141
  relDir: string,
98
142
  traceName: string,
143
+ artifactId: string,
99
144
  totalSteps: number,
100
145
  ): string[] {
101
- const baseDir = [relDir, '_traces', traceName].filter(Boolean).join('/');
102
- const outputPaths = [`${baseDir}/manifest.json`];
146
+ const outputPaths = [
147
+ buildTraceOutputPath(relDir, traceName, artifactId, 'manifest.json'),
148
+ ];
103
149
  for (
104
150
  let chunkIndex = 0;
105
151
  chunkIndex * DEFAULT_CHUNK_SIZE < totalSteps;
106
152
  chunkIndex++
107
153
  ) {
108
- outputPaths.push(`${baseDir}/chunk-${chunkIndex}.json`);
154
+ outputPaths.push(
155
+ buildTraceOutputPath(
156
+ relDir,
157
+ traceName,
158
+ artifactId,
159
+ `chunk-${chunkIndex}.json`,
160
+ ),
161
+ );
109
162
  }
110
163
  return outputPaths;
111
164
  }
@@ -148,18 +201,18 @@ export function materializeTraceOutputs({
148
201
  export function buildManifestUrl({
149
202
  relDir,
150
203
  traceName,
204
+ artifactId,
151
205
  applyBasePath,
152
206
  }: {
153
207
  relDir: string;
154
208
  traceName: string;
209
+ artifactId: string;
155
210
  applyBasePath: (subPath: string) => string;
156
211
  }): string {
157
212
  return applyBasePath(
158
213
  normalizeOutputPath(
159
214
  '/' +
160
- [relDir, '_traces', traceName, 'manifest.json']
161
- .filter(Boolean)
162
- .join('/'),
215
+ buildTraceOutputPath(relDir, traceName, artifactId, 'manifest.json'),
163
216
  ),
164
217
  );
165
218
  }
@@ -42,6 +42,7 @@ export interface TraceContext {
42
42
 
43
43
  export interface TraceResult {
44
44
  manifestUrl: string;
45
+ artifactId: string;
45
46
  highlightedSource: string;
46
47
  totalSteps: number;
47
48
  mtime: number;
@@ -141,6 +142,7 @@ export function createTraceHelpers(context: TraceContext): {
141
142
  const outputPaths = getTraceOutputPaths(
142
143
  relDir,
143
144
  traceName,
145
+ cached.artifactId,
144
146
  cached.totalSteps,
145
147
  );
146
148
  if (
@@ -155,7 +157,12 @@ export function createTraceHelpers(context: TraceContext): {
155
157
  }
156
158
  return {
157
159
  ...cached,
158
- manifestUrl: buildManifestUrl({ relDir, traceName, applyBasePath }),
160
+ manifestUrl: buildManifestUrl({
161
+ relDir,
162
+ traceName,
163
+ artifactId: cached.artifactId,
164
+ applyBasePath,
165
+ }),
159
166
  };
160
167
  }
161
168
  }
@@ -176,32 +183,42 @@ export function createTraceHelpers(context: TraceContext): {
176
183
  );
177
184
  const traceOutputDir = path.join(distDir, relDir, '_traces', traceName);
178
185
 
179
- const manifest = chunkTraceOutput(
186
+ const traceOutput = chunkTraceOutput(
180
187
  output,
181
188
  traceOutputDir,
189
+ relDir,
190
+ traceName,
182
191
  sourceFileNameForTrace(sourceFile),
183
192
  source,
184
193
  { chunkSize: DEFAULT_CHUNK_SIZE, ignoreFields },
185
194
  );
186
- for (const outputPath of getTraceOutputPaths(
187
- relDir,
188
- traceName,
189
- manifest.totalSteps,
190
- )) {
195
+ for (const outputPath of traceOutput.outputPaths) {
191
196
  dependencyCollector?.generatedOutputPaths?.add(outputPath);
192
197
  }
193
198
 
194
- const manifestUrl = buildManifestUrl({ relDir, traceName, applyBasePath });
199
+ const manifestUrl = buildManifestUrl({
200
+ relDir,
201
+ traceName,
202
+ artifactId: traceOutput.artifactId,
203
+ applyBasePath,
204
+ });
195
205
 
196
- const totalSteps = manifest.totalSteps;
206
+ const totalSteps = traceOutput.manifest.totalSteps;
197
207
  const mtime = fs.statSync(sourceFilePath).mtimeMs;
198
208
  cache.set(sourceFilePath, {
199
209
  manifestUrl,
210
+ artifactId: traceOutput.artifactId,
200
211
  highlightedSource,
201
212
  totalSteps,
202
213
  mtime,
203
214
  });
204
- return { manifestUrl, highlightedSource, totalSteps, mtime };
215
+ return {
216
+ manifestUrl,
217
+ artifactId: traceOutput.artifactId,
218
+ highlightedSource,
219
+ totalSteps,
220
+ mtime,
221
+ };
205
222
  }
206
223
 
207
224
  return {
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { bundle } from '../bundle';
3
+ import { bundle, getBundleNaming } from '../bundle';
4
4
  import { copyFonts } from '../generate-fonts';
5
5
  import { copyKatexAssets } from '../generate-katex-assets';
6
6
  import { generateFavicons } from '../generate-favicon';
@@ -42,7 +42,7 @@ export async function bundleWatchAssets(
42
42
  const reloadAssets = await Bun.build({
43
43
  entrypoints: [RELOAD_CLIENT_PATH],
44
44
  outdir: outputDir,
45
- naming: '[name].bundle.[ext]',
45
+ naming: getBundleNaming(),
46
46
  sourcemap: 'inline',
47
47
  });
48
48
  return [
@@ -6,6 +6,7 @@ import type {
6
6
 
7
7
  export interface TraceCacheEntry {
8
8
  manifestUrl: string;
9
+ artifactId: string;
9
10
  highlightedSource: string;
10
11
  totalSteps: number;
11
12
  mtime: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.13.4",
3
+ "version": "1.13.7",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/_base.scss CHANGED
@@ -38,12 +38,13 @@ body {
38
38
  * Enable Inter's features:
39
39
  * liga - standard ligatures
40
40
  * calt - contextual alternates
41
- * ss03 - round quotes & commas (legibility)
42
- * cv01 - alternate 1 (style)
43
- * cv05 - lower case L with tail (legibility)
44
- * cv10 - capital G with spur (style)
41
+ * ss07 - square punctuation
42
+ * ss08 - square quotes
43
+ * cv01 - alternate 1
44
+ * cv08 - uppercase I with serif
45
+ * cv10 - capital G with spur
45
46
  */
46
- font-feature-settings: 'liga', 'calt', 'ss03', 'cv01', 'cv05', 'cv10';
47
+ font-feature-settings: 'liga', 'calt', 'ss07', 'ss08', 'cv01', 'cv08', 'cv10';
47
48
  line-height: var(--line-height);
48
49
  color: var(--fg-color);
49
50
  background-color: var(--bg-color);
@@ -9,6 +9,8 @@ interface SlidesPresentDetail {
9
9
  slideIndex?: number;
10
10
  }
11
11
 
12
+ const FULLSCREEN_STORAGE_KEY = 'slidesFullscreen';
13
+
12
14
  function isInteractive(
13
15
  view: Window & typeof globalThis,
14
16
  target: EventTarget | null,
@@ -83,6 +85,16 @@ export default function mountSlides(window: Window): void | (() => void) {
83
85
  presentButton.disabled = false;
84
86
  }
85
87
  if (fullscreenCheckbox) {
88
+ try {
89
+ const stored = window.localStorage.getItem(FULLSCREEN_STORAGE_KEY);
90
+ if (stored === 'false') {
91
+ fullscreenCheckbox.checked = false;
92
+ } else if (stored === 'true') {
93
+ fullscreenCheckbox.checked = true;
94
+ }
95
+ } catch {
96
+ // ignored
97
+ }
86
98
  fullscreenCheckbox.disabled = false;
87
99
  }
88
100
 
@@ -365,6 +377,20 @@ export default function mountSlides(window: Window): void | (() => void) {
365
377
  exitPresentation();
366
378
  }
367
379
 
380
+ function handleFullscreenPreferenceChange(): void {
381
+ if (!fullscreenCheckbox) {
382
+ return;
383
+ }
384
+ try {
385
+ window.localStorage.setItem(
386
+ FULLSCREEN_STORAGE_KEY,
387
+ String(fullscreenCheckbox.checked),
388
+ );
389
+ } catch {
390
+ // Ignore storage errors so the checkbox remains usable.
391
+ }
392
+ }
393
+
368
394
  function handleFullscreenChange(): void {
369
395
  if (
370
396
  isPresenting &&
@@ -460,6 +486,10 @@ export default function mountSlides(window: Window): void | (() => void) {
460
486
  }
461
487
 
462
488
  presentButton?.addEventListener('click', handlePresentClick);
489
+ fullscreenCheckbox?.addEventListener(
490
+ 'change',
491
+ handleFullscreenPreferenceChange,
492
+ );
463
493
  closeButton.addEventListener('click', handleCloseClick);
464
494
  document.addEventListener('fullscreenchange', handleFullscreenChange);
465
495
  window.addEventListener('keydown', handleKeydown);
@@ -470,6 +500,10 @@ export default function mountSlides(window: Window): void | (() => void) {
470
500
  return () => {
471
501
  exitPresentation();
472
502
  presentButton?.removeEventListener('click', handlePresentClick);
503
+ fullscreenCheckbox?.removeEventListener(
504
+ 'change',
505
+ handleFullscreenPreferenceChange,
506
+ );
473
507
  closeButton.removeEventListener('click', handleCloseClick);
474
508
  document.removeEventListener('fullscreenchange', handleFullscreenChange);
475
509
  window.removeEventListener('keydown', handleKeydown);
package/src/style.scss CHANGED
@@ -121,7 +121,7 @@ a.footnote-ref {
121
121
 
122
122
  a.footnote-ref,
123
123
  .footnote-marker {
124
- font-feature-settings: 'ss06';
124
+ font-feature-settings: 'ss06'; // squared numbers
125
125
  }
126
126
 
127
127
  .footnote-item {
@@ -58,13 +58,20 @@ async function goToStep(
58
58
  }
59
59
 
60
60
  function updateSourceHighlight(panel: HTMLElement, currentLine: number): void {
61
- panel.querySelectorAll('.line-number').forEach(ln => {
62
- ln.classList.remove('trace-line-active');
63
- });
64
- const el = panel.querySelector(`.line-number[data-line="${currentLine}"]`);
65
- if (el) {
66
- el.classList.add('trace-line-active');
67
- const elRect = el.getBoundingClientRect();
61
+ panel
62
+ .querySelectorAll(
63
+ '.code-row.trace-line-active, .line-number.trace-line-active',
64
+ )
65
+ .forEach(el => {
66
+ el.classList.remove('trace-line-active');
67
+ });
68
+ const lineNumber = panel.querySelector(
69
+ `.line-number[data-line="${currentLine}"]`,
70
+ );
71
+ const row = lineNumber?.closest('.code-row') as HTMLElement | null;
72
+ if (row) {
73
+ row.classList.add('trace-line-active');
74
+ const elRect = row.getBoundingClientRect();
68
75
  const panelRect = panel.getBoundingClientRect();
69
76
  panel.scrollTop += elRect.top - panelRect.top - panel.clientHeight / 2;
70
77
  }
@@ -112,16 +112,17 @@
112
112
  }
113
113
  }
114
114
 
115
- .line-number.trace-line-active {
116
- font-weight: bold;
117
- color: var(--fg-color);
118
-
119
- &::before {
120
- position: absolute;
121
- left: 0;
122
- margin-right: 0.25em;
123
- line-height: 1.3;
124
- content: '\25B6';
115
+ .code-row.trace-line-active {
116
+ background: var(--bg-trace-line-active);
117
+
118
+ .line-number {
119
+ font-weight: bold;
120
+ color: var(--fg-color);
121
+ background: var(--bg-trace-line-active);
122
+ }
123
+
124
+ code {
125
+ background: none;
125
126
  }
126
127
  }
127
128
  }
@@ -32,6 +32,7 @@
32
32
  --bg2-color-translucent: hsl(
33
33
  var(--tint-hue) calc(12% * var(--tint-amount)) 92% / 66%
34
34
  );
35
+ --bg-trace-line-active: <%= bgTraceLineActive %>;
35
36
  --box-shadow: 0 2px 9px
36
37
  hsl(var(--tint-hue) calc(85% * var(--tint-amount)) 10% / 25%);
37
38
  --backdrop-filter: blur(10px);
@@ -87,6 +88,7 @@
87
88
  --bg2-color-translucent: hsl(
88
89
  var(--tint-hue) calc(12% * var(--tint-amount)) 12% / 66%
89
90
  );
91
+ --bg-trace-line-active: <%= bgTraceLineActiveDark %>;
90
92
  --box-shadow: 0 3px 8px
91
93
  hsl(var(--tint-hue) calc(33% * var(--tint-amount)) 8% / 70%);
92
94
  --link-active-color: #fff;
@@ -7,7 +7,7 @@
7
7
  <div class="slides-header" data-pagefind-ignore>
8
8
  <button type="button" data-slides-present disabled>Present</button>
9
9
  <label>
10
- <input type="checkbox" data-slides-fullscreen checked disabled>
10
+ <input id="slides-fullscreen" type="checkbox" data-slides-fullscreen checked disabled>
11
11
  Full screen
12
12
  </label>
13
13
  </div>